Reagent React Wrapper Components
Table of Contents
Overview
Reagent provides a minimalist interface for building React components in ClojureScript, while Re-frame adds an application architecture layer for managing state and side effects. Together, they form the foundation for building complex single-page applications with predictable data flow. This document covers component patterns, state management strategies, and integration techniques for wrapping existing React component libraries.
Background
Reagent was created by Dan Holmsand in 2013 as a simple way to use React from ClojureScript, representing components as plain functions returning Hiccup-style vectors. Re-frame, developed by Mike Thompson starting in 2015, builds on Reagent to provide a unidirectional data flow architecture inspired by Elm and Redux. The combination has become the dominant approach for ClojureScript frontend development.
Key Concepts
Reagent Component Forms
Three ways to define components with different lifecycle behaviors:
;; Form-1: Simple function (re-renders on any change) (defn greeting [name] [:div "Hello, " name]) ;; Form-2: Function returning function (closure for local state) (defn counter [] (let [count (reagent/atom 0)] (fn [] [:div [:span @count] [:button {:on-click #(swap! count inc)} "+"]]))) ;; Form-3: Full lifecycle (for React interop) (defn canvas-component [] (reagent/create-class {:component-did-mount (fn [this] (setup-canvas (reagent/dom-node this))) :reagent-render (fn [] [:canvas])}))
Re-frame Architecture
Event-driven state management with subscriptions and effects:
;; Event handler (re-frame/reg-event-db :increment (fn [db [_ amount]] (update db :count + amount))) ;; Subscription (re-frame/reg-sub :count (fn [db _] (:count db))) ;; Component using subscription (defn counter-display [] (let [count @(re-frame/subscribe [:count])] [:div "Count: " count]))
Wrapping React Libraries
Adapt JavaScript React components for Reagent:
(def MaterialButton (reagent/adapt-react-class (.-Button (js/require "@mui/material")))) (defn my-button [] [MaterialButton {:variant "contained" :color "primary"} "Click Me"])
Component Composition Patterns
;; Higher-order component (defn with-loading [component] (fn [props] (if (:loading props) [:div.spinner "Loading..."] [component props]))) ;; Render props pattern (defn mouse-tracker [render-fn] (let [pos (reagent/atom {:x 0 :y 0})] (fn [] [:div {:on-mouse-move #(reset! pos {:x (.-clientX %) :y (.-clientY %)})} [render-fn @pos]])))
Implementation
Project Dependencies
;; deps.edn {:deps {reagent/reagent {:mvn/version "1.2.0"} re-frame/re-frame {:mvn/version "1.3.0"}}}
Application Bootstrap
(ns app.core (:require [reagent.dom :as rdom] [re-frame.core :as rf])) (rf/reg-event-db :initialize (fn [_ _] {:count 0})) (defn app [] [:div [counter-display] [:button {:on-click #(rf/dispatch [:increment 1])} "+"]]) (defn init [] (rf/dispatch-sync [:initialize]) (rdom/render [app] (.getElementById js/document "app")))
Effect Handlers for Side Effects
(rf/reg-fx :http-xhrio (fn [{:keys [uri on-success on-failure]}] (-> (js/fetch uri) (.then #(.json %)) (.then #(rf/dispatch (conj on-success %))) (.catch #(rf/dispatch (conj on-failure %))))))
References
Notes
- Use
reagent/trackfor derived computations that cache like subscriptions - Prefer Form-1 components unless you need local state or lifecycle methods
- Re-frame subscriptions are automatically cached and deduplicated
- Consider
re-frame-http-fxfor HTTP effects rather than custom implementations - Use
re-frame-10xdevtools for time-travel debugging in development