Skip to content

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 day8/re-frame2-reagent; Artefact and floor has the coordinate and the React and Reagent versions it needs.

(ns my-app.core
  (:require [re-frame.core :as rf]
            [re-frame.adapter.reagent :as reagent-adapter]))
(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 :rf/default}
       [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 reads the surrounding frame-root or frame-provider while it renders, as a registered view does. A callback that runs after the render, such as an :on-click or a timer, has no frame to read, so an ambient rf/dispatch there raises :rf.error/no-frame-context; capture the frame during the render with rf/capture-frame (a reg-view injects ops captured this way), 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:
    {:kind                      :rf.adapter/reagent
     :make-state-container      …
     :read-container            …
     :replace-container!        …
     :subscribe-container       …
     :make-derived-value        …
     :render                    …
     :render-to-string          …
     :register-context-provider …
     :flush-render!             …
     :dispose-adapter!          …}
    
  • Description: The adapter map you pass to rf/init! to install Reagent as the substrate (stock reagent.core / reagent.dom.client).
    • Installation is explicit: there is no default adapter and no keyword form, so require this namespace and pass adapter at 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.
    • Frames are scoped with core's frame-provider and frame-root, whose children are trailing hiccup: [rf/frame-provider {:frame …} & children].
  • Example:
    (rf/init! reagent-adapter/adapter)   ;; install the substrate once, at boot
    

Artefact and floor

The adapter ships in day8/re-frame2-reagent, which carries stock Reagent (reagent.core, reagent.dom.client, reagent.dom.server). It needs React 19 and runs on Reagent 2.x; there is no React 17/18 or Reagent 1.x path.

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. UIx releases them within one macrotask, so prefer it where those patterns matter.

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:
    (client-root) → handle
    
  • 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 first render! through the handle creates (or hydrates) the React root.
    • The handle is opaque: pass it to render! and unmount! and nothing else.
  • Example:
    (defonce app-root (reagent-adapter/client-root))   ;; inert until the first render!
    

render!

  • Kind: function
  • Signature:
    (render! handle render-tree mount-point)      → nil
    (render! handle render-tree mount-point opts) → nil
    
  • Description: Renders render-tree (hiccup) into the DOM element mount-point through handle: 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 inside mount-point instead (see re-frame.ssr). Later calls never create a second root or hydrate a second time.
    • A hydrating first call also reads :on-recoverable-error, which React calls with each error it recovers from, and :identifier-prefix, which React's useId needs to match the server's and must be the prefix the server rendered under. In every build, a hydration mismatch React recovers from is reported first as an always-on :rf.ssr/hydration-mismatch error record that reaches your error sinks, and in a development build also as the trace of the same id; then your callback runs, or React's default report.
    • Because later calls update the same root, one call serves as both the boot path and the ^:dev/after-load hook. mount-point is read on the first call only.
    • After unmount!, or after rf/destroy-adapter! has released the root, the next render! mounts afresh.
    • Call rf/init! before the first render!. 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-disposed after rf/destroy-adapter!. Install an adapter again before rendering afresh.
  • Example:
    (reagent-adapter/render! app-root [rf/frame-root {:id :rf/default} [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 :rf/default} [counter]] el {:hydrate? true})
    ;; Hydration has already installed :rf/default; later renders update this root.
    

unmount!

  • Kind: function
  • Signature:
    (unmount! handle) → nil
    
  • Description: Unmounts the React root handle holds and returns the handle to inert, so a later render! mounts afresh. Returns nil.
    • Unmounting releases the view tree; it does not destroy the frames used by that tree. A frame-root reuses its live frame on remount. If the app owns a frame that should end here, call rf/destroy-frame! separately.
    • Idempotent: a second call, or a call after rf/destroy-adapter! has released the root, does nothing.
  • Example:
    (reagent-adapter/unmount! app-root)   ;; releases the root; a repeat call is a no-op
    

Server-side rendering

set-hiccup-emitter!

  • Kind: function
  • Signature:
    (set-hiccup-emitter! f)
    
  • Description: Installs the function the adapter's render-to-string uses to turn a render tree into HTML. Requiring re-frame.ssr installs it for you, so you rarely call this directly.
    • f takes the render tree and an opts map, and returns an HTML string.
    • Last call wins; pass nil to reset.
    • With no emitter installed, render-to-string raises :rf.error/no-hiccup-emitter-bound.
    • rf/destroy-adapter! clears the installed emitter. The next rf/init! re-installs re-frame.ssr's emitter when that namespace is loaded, and otherwise leaves the slot empty. An emitter you install before rf/init! is kept, so install a custom emitter again after re-initialising.
    • The UIx adapter has the same function.
  • Example:
    (reagent-adapter/set-hiccup-emitter! (fn [tree _opts] (str tree)))
    (reagent-adapter/set-hiccup-emitter! nil)   ;; reset
    

See also

  • re-frame.adapter.uix — the hooks-first React adapter, with the same client-root functions.
  • re-frame.ssr — server-side rendering; installs set-hiccup-emitter! for you.
  • Use UIx — choosing a substrate, and moving an app's views to UIx.
  • Views — why the substrate only shows up in the view body.
  • Adapter and substrate in the glossary.