Reagent: A Lean, Reactive Path to Stateful Lists in ClojureScript
When you need a dynamic list UI that stays in sync with your data, Reagent lets you write declarative components with minimal boilerplate. Learn how atoms, cursors, and lifecycle hooks make state management painless, see a Todo‑list example, and weigh the trade‑offs before you commit.
05 Aug 2025, 17:47 UTC

Stateful List UI – The Problem
In a typical single‑page application you often need a list that can be added to, edited, or removed from at run time. The classic approach in React is to keep the list in component state, update it with setState, and let the framework diff the DOM. In ClojureScript, however, you’re used to immutable data structures and functional updates. Bridging that gap without writing boilerplate can feel like a chore.
Why Reagent?
Reagent is a lightweight wrapper that lets you write React components as plain ClojureScript functions returning Hiccup syntax. The core idea is that the component automatically re‑renders whenever any atom or cursor it dereferences changes. This eliminates the need for manual diffing logic and keeps the UI in sync with immutable data.
Core Concepts
Atoms – The Source of Truth
An atom is a mutable reference that holds immutable values. When you reset! or swap! an atom, Reagent notices the change and triggers a re‑render of any component that dereferenced it.
;; Define a global atom holding a vector of todo items
(defonce *todos* (reagent.core/atom []))
Cursors – Scoped State
Cursors let you focus on a sub‑path of an atom, useful for passing only the relevant slice to a child component.
;; Create a cursor pointing to the :tasks key in the global state
(def tasks-cursor (reagent.core/cursor *todos* [:tasks]))
Component Lifecycle Hooks
Reagent components can be plain functions, but you can add a map of lifecycle hooks if you need to run code after mounting or before unmounting.
;; Example of a component with a componentDidMount hook
(defn my-component []
{:component-did-mount (fn [] (js/console.log "Mounted"))
:reagent-render (fn [] [:div "Hello"])})
Worked Example – Todo List
Below is a minimal Todo‑list that demonstrates adding, editing, and deleting items using Reagent atoms. Run it in a fresh ClojureScript project with the Reagent dependency (e.g., com.github.liquidz/reagent).
;; src/app/core.cljs
(ns app.core
(:require [reagent.core :as r]))
;; Global atom holding the list of todos
(defonce *todos* (r/atom []))
;; Component for a single todo item
(defn todo-item [{:keys [id text]}]
(r/with-let [editing (r/atom false)
edit-text (r/atom text)]
(fn []
[:li
[:span (when @editing
[:input {:type "text"
:value @edit-text
:on-change #(reset! edit-text (.. % -target -value))}])
(if @editing
@edit-text
text)]
[:button {:on-click #(swap! *todos* (fn [todos]
(mapv (fn [t]
(if (= (:id t) id)
(assoc t :text @edit-text)
t))
todos)))}
"Save"]
[:button {:on-click #(swap! *todos* (fn [todos]
(remove (fn [t] (= (:id t) id)) todos))) }
"Delete"]]))
;; Main component
(defn todo-app []
(let [new-text (r/atom "")
add-todo (fn []
(swap! *todos* conj {:id (random-uuid) :text @new-text}))]
(fn []
[:div
[:h1 "Reagent Todo List"]
[:input {:type "text"
:placeholder "New todo"
:value @new-text
:on-change #(reset! new-text (.. % -target -value))}]
[:button {:on-click add-todo} "Add"]
[:ul (for [t @*todos*] ^{:key (:id t)} [todo-item t])]])))
;; Mount the app
(r/render [todo-app] (.getElementById js/document "app"))
Key points to verify:
- Adding a new item updates the UI instantly.
- Editing an item’s text and clicking
Saveupdates the list. - Clicking
Deleteremoves the item.
Open the browser console and React DevTools to confirm that component updates are driven by atom changes and that no manual re‑render calls are needed.
Trade‑offs & Limitations
| Aspect | Pros | Cons |
|---|---|---|
| Learning Curve | Minimal if you know ClojureScript; Reagent syntax is concise. | Requires understanding of React’s rendering model; unfamiliarity can hide bugs. |
| Interop with Plain JS | Easy via js/ interop. | Stale UI updates if you forget to wrap native data in atoms. |
| Performance | Automatic diffing; no manual setState. | Atom updates can trigger deep component re‑renders; careful atom structuring is needed. |
| Tooling | Rich ClojureScript compiler optimizations. | Debugging can be harder due to macro expansion. |
When to Adopt Reagent
- You already use ClojureScript and want a React‑style UI without learning a new framework.
- Your application benefits from immutable data and a single source of truth.
- You need rapid prototyping of dynamic lists or forms.
Before committing, prototype a small component to ensure that atom updates trigger the expected UI changes. If your team is comfortable with ClojureScript and React concepts, Reagent can reduce boilerplate and keep the UI declarative.
In summary, Reagent offers a pragmatic bridge between ClojureScript’s functional data model and React’s component architecture, making stateful list UIs straightforward and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.