re-frame.adapter.reagent¶
Use this namespace to run a re-frame2 app on Reagent. It provides the adapter you pass to rf/init! at boot, and the client-root, render! and unmount! functions your entry namespace mounts the app through.
Pick it when your views are hiccup; the Core guide uses it. If your components are React function components written with UIx hooks, use re-frame.adapter.uix instead. It ships in two artefacts, day8/re-frame2-reagent (full) and day8/reagent-slim (slim), and both publish this namespace; the variants, and when to pick slim, are compared under Full and slim.
(rf/reg-event :counter/inc
(fn [{:keys [db]} _]
{:db (update db :counter/value (fnil inc 0))}))
(rf/reg-sub :counter/value
(fn [db _] (:counter/value db 0)))
(rf/reg-view counter []
[:button {:on-click #(dispatch [:counter/inc])}
"Clicked " @(subscribe [:counter/value]) " times"])
(defonce app-root (reagent-adapter/client-root))
(defn ^:dev/after-load mount! []
(when-let [el (and (exists? js/document)
(js/document.getElementById "app"))]
(reagent-adapter/render! app-root
[rf/frame-root {:id :app/main}
[counter]]
el)))
(defn run []
(rf/init! reagent-adapter/adapter)
(mount!))
Everything else a Reagent app calls is on re-frame.core: reg-view, the frame-root and frame-provider components, capture-frame, with-frame, and the lifecycle functions init!, destroy-adapter! and current-adapter. There are no hooks here: a view registered with reg-view gets dispatch and subscribe injected instead. A plain Reagent function rendered as a component cannot read the surrounding frame-root or frame-provider, so an ambient rf/subscribe or rf/dispatch in its body raises :rf.error/no-frame-context; register it with reg-view (or reg-view*), or name the frame with {:frame …}. The dependency is one-way: this namespace requires re-frame.core, and core never requires it. Boot and mount an app walks through the entry namespace.
Adapter spec¶
adapter¶
- Kind: var (map)
- Signature:
- Description: The adapter map you pass to
rf/init!to install Reagent as the substrate (stockreagent.core/reagent.dom.client).- Installation is explicit: there is no default adapter and no keyword form, so require this namespace and pass
adapterat the call site. - Once it is installed,
(rf/current-adapter)returns this map, and its presence is how you ask whether an adapter is installed.(:kind (rf/current-adapter))reads:rf.adapter/reagent, or:rf.adapter/reagent-slimon the slim variant. - Frames are scoped with core's
frame-providerandframe-root, whose children are trailing hiccup:[rf/frame-provider {:frame …} & children].
- Installation is explicit: there is no default adapter and no keyword form, so require this namespace and pass
- Example:
Full and slim¶
Reagent ships in two variants. Both publish their adapter as re-frame.adapter.reagent, so the require and the init! line are the same; the Maven coordinate in your deps.edn selects the variant, and a build depends on one of them only. Start on full, and move to slim once you have measured that bundle size matters.
| Variant | Maven coordinate | Includes | Use when |
|---|---|---|---|
| Full | day8/re-frame2-reagent |
stock Reagent (reagent.core, reagent.dom.client, reagent.dom.server) |
the default; any app that uses stock Reagent APIs the slim rewrite leaves out, such as reagent.dom.server |
| Slim | day8/reagent-slim |
the reagent2 rewrite; static HTML export via a pure-CLJS reagent2.dom.server, no react-dom/server |
browser-only bundles where size is measured to matter; ~7–10 KB gzipped smaller (up to ~22–27 KB for a bundle that uses the HTML-export path) |
Both variants need React 19, and full runs on Reagent 2.x. There is no React 17/18 or Reagent 1.x path.
A :git/sha dependency on this repository is the exception to the shared name. The repository carries both adapters on one classpath, so there the slim adapter is re-frame.adapter.reagent-slim; the published jar renames it to re-frame.adapter.reagent.
A build on the slim variant includes neither stock Reagent nor react-dom/server, so app code that requires a reagent.* namespace requires its reagent2.* counterpart instead (reagent2.core, reagent2.dom.client). Switching an app from full to slim is a four-line change, shown in Use UIx or reagent-slim.
On full, a view rendered in a pass that React discards before committing keeps its subscriptions for the life of the page. Such passes include a Suspense boundary suspending on first mount, an error boundary catching on mount, and a hidden Activity that is never shown. Each change to one of those subscriptions force-updates the never-mounted instance, and React's development build warns about it. Stock Reagent gives the adapter no commit signal to release them. Slim and UIx release them within one macrotask, so prefer one of them where those patterns matter.
Slim rejects hiccup it cannot render with a tagged error, where stock Reagent throws its own untagged one.
- Errors, on slim only:
- At render:
:rf.error/template-empty-vector:[]as hiccup.:rf.error/template-bad-tag: a head that is not a tag (a keyword, string or symbol), a component class or a function.:rf.error/invalid-hiccup-head: a keyword head in the reserved:rf/*or:rf.<name>/*namespaces. No reserved head renders on the client.
- When
reagent2.core/create-classcreates the class::rf.error/create-class-key-unsupported: a key outside the seven it accepts, which are:reagent-render,:component-did-mount,:component-did-update,:component-will-unmount,:get-snapshot-before-update,:component-did-catchand:display-name.:rf.error/create-class-missing-render: a spec with no:reagent-render.
- At render:
The client root¶
A browser app needs one React root for the life of the page: created once, updated on every hot reload, released on teardown. client-root, render! and unmount! manage that root, so your entry namespace never creates one or touches reagent.dom.client. Allocate the handle under a defonce and call render! from the ^:dev/after-load hook, as in the example at the top of this page, with run as the build's :init-fn. On a hot reload shadow-cljs calls mount! again: the defonce keeps the handle, render! updates the same root, and frame-root reuses the live frame without re-running :initial-events, so app-db survives the reload.
The raw React root is never exposed. rf/destroy-adapter! also releases it, exactly once.
client-root¶
- Kind: function
- Signature:
- Description: Returns a new, inert client-root handle. It does no DOM work, so it is safe at namespace load under a
defonce, in tests and on Node; the firstrender!through the handle creates (or hydrates) the React root.- The handle is opaque: pass it to
render!andunmount!and nothing else.
- The handle is opaque: pass it to
- Example:
render!¶
- Kind: function
- Signature:
- Description: Renders
render-tree(hiccup) into the DOM elementmount-pointthroughhandle: the first call creates the React root, and every later call updates it. Returns nil.- With
{:hydrate? true}the first call hydrates the server-rendered markup already insidemount-pointinstead (seere-frame.ssr). Later calls never create a second root or hydrate a second time.:hydrate?is the onlyoptskey. - Because later calls update the same root, one call serves as both the boot path and the
^:dev/after-loadhook.mount-pointis read on the first call only. - After
unmount!, or afterrf/destroy-adapter!has released the root, the nextrender!mounts afresh. - Call
rf/init!before the firstrender!.render!does not check for an adapter, but the first frame or subscription the tree creates raises:rf.error/no-adapter-installed, or:rf.error/adapter-disposedafterrf/destroy-adapter!. Install an adapter again before rendering afresh.
- With
- Example:
(reagent-adapter/render! app-root [rf/frame-root {:id :app/main} [counter]] el) ;; Later calls update the same root and reuse its frame. ;; Alternative boot for an SSR page: hydrate on this handle's FIRST render. (defonce hydrated-root (reagent-adapter/client-root)) (reagent-adapter/render! hydrated-root [rf/frame-provider {:frame :app/main} [counter]] el {:hydrate? true}) ;; Hydration has already installed :app/main; later renders update this root.
unmount!¶
- Kind: function
- Signature:
- Description: Unmounts the React root
handleholds and returns the handle to inert, so a laterrender!mounts afresh. Returns nil.- Unmounting releases the view tree; it does not destroy the frames used by that tree. A
frame-rootreuses its live frame on remount. If the app owns a frame that should end here, callrf/destroy-frame!separately. - Idempotent: a second call, or a call after
rf/destroy-adapter!has released the root, does nothing.
- Unmounting releases the view tree; it does not destroy the frames used by that tree. A
- Example:
Test helpers¶
flush-views!¶
- Kind: function
- Signature:
- Description: Flushes pending Reagent renders synchronously inside React's
act(), for tests. Returns nil. Use it when a test mounts into a real DOM and must let React commit before it reads the DOM; most Reagent view tests need no DOM, and call the view and walk its hiccup withre-frame.test-helpersinstead.- 0-arity: drains the queued renders and effects.
- 1-arity: runs the thunk
f, then drains the renders, insideact(). - When
act()is not available in the current React build, it flushes without it:fstill runs and the render queue still drains. - It settles what the test drives: a mount, or a
dispatch-syncrun insidef. Adispatchqueues on the router instead, so wait for it withre-frame.test-support/poll-until. Do not call it from inside adispatch-synchandler; it runs a render. - React's
act()expects the test to setglobalThis.IS_REACT_ACT_ENVIRONMENTtotruewhile it drives React throughflush-views!, and to set it back while it waits on React's own schedule. Test a view §4 shows the pattern. re-frame.adapter.uixpublishes aflush-views!with the same name and nil return; withoutact(), that one does nothing and does not runf.- On slim,
reagent2.dom.client/flush-views!is a different function: it takes no argument and returns a Promise that resolves when the drain has finished (nilwhenact()is unavailable, and in a production build), for a test that must await Suspense ordering.
- Example:
Server-side rendering¶
set-hiccup-emitter!¶
- Kind: function
- Signature:
- Description: Installs the function the adapter's
render-to-stringuses to turn a render tree into HTML. Requiringre-frame.ssrinstalls it for you, so you rarely call this directly.ftakes the render tree and an opts map, and returns an HTML string.- Last call wins; pass
nilto reset. - With no emitter installed,
render-to-stringraises:rf.error/no-hiccup-emitter-bound. rf/destroy-adapter!clears the installed emitter. The nextrf/init!re-installsre-frame.ssr's emitter when that namespace is loaded, and otherwise leaves the slot empty. An emitter you install beforerf/init!is kept, so install a custom emitter again after re-initialising.- The UIx adapter has the same function.
- Example:
See also¶
re-frame.adapter.uix— the hooks-first React adapter, with the same client-root functions.re-frame.ssr— server-side rendering; installsset-hiccup-emitter!for you.- Use UIx or reagent-slim — choosing a substrate, including the switch to slim.
- Views — why the substrate only shows up in the view body.
- Adapter and substrate in the glossary.