Managing Frontend State with ClojureScript: The re-frame Pattern
Learn how to eliminate state synchronization bugs in ClojureScript using the re-frame pattern's unidirectional data flow, centralized DB, and optimized subscriptions.
27 Nov 2025, 00:30 UTC

The State Synchronization Struggle
In complex frontend applications, the primary source of bugs is often "out-of-sync" state. When a piece of data—like a user's profile name—is stored in multiple components, updating it in one place often leaves others displaying stale information. This leads to a fragmented UI where the user sees conflicting data depending on which tab or modal is open.
The solution is to move state out of the UI components entirely. By using a centralized data store and a unidirectional flow, you ensure that the UI is simply a visual representation of a single source of truth.
The re-frame Architecture
re-frame is a functional state management framework for ClojureScript that builds on top of Reagent (a ClojureScript wrapper for React). It implements a strict loop: Events → Effects → DB → Subscriptions → View.
The Central DB
The application state is stored in a single, immutable map called the db. Because it is immutable, you can track every single change to the state over time, making debugging significantly easier than with mutable objects.
Events and State Transitions
You never modify the db directly. Instead, you dispatch events. An event is a simple keyword or vector (e.g., [:update-user-name "Alice"]). A registered event handler receives the current state and the event data, then returns a new version of the state.
Subscriptions for Performance
To prevent the entire application from re-rendering on every tiny state change, re-frame uses subscriptions (reg-sub). These act as selectors that extract a specific slice of the state. A component only re-renders if the specific value it subscribes to changes, regardless of how many other updates are happening in the rest of the db.
Worked Example: A Simple Task Tracker
This example demonstrates a basic unidirectional flow where a user adds a task to a list. This requires re-frame and reagent dependencies in your shadow-cljs.edn.
(ns app.core
(:require [reframe.core :as rf]
[reagent.core :as r]))
;; 1. Define the initial state
(rf/create-db
{:tasks []})
;; 2. Define the event to update the state
;; This is a pure function: (current-db event-value) -> new-db
(rf/reg-event-db
:add-task
(fn [db [?_ task-text]]
(update db :tasks conj task-text)))
;; 3. Define a subscription to get the tasks
(rf/reg-sub
:tasks
(fn [db _]
(:tasks db)))
;; 4. The UI Component
(defn task-view []
(let [tasks (rf/subscribe [:tasks])]
(fn []
(let [task-input (r/atom "")]
[:div
[:input {:type "text"
:value @task-input
:on-change #(set! task-input (.. % -target -value))}]
[:button {:on-click #(do
(rf/dispatch [:add-task @task-input])
(reset! task-input ""))}
"Add Task"]
[:ul
(for [t @tasks]
[:li t])]])))))
(r/render [task-view] (.getElementById js/document "app"))
Handling Side Effects with fx
Pure functions cannot make API calls or set timers. For these tasks, re-frame provides reg-event-fx. Instead of returning a new DB, these handlers return a map of "effects." For example, you can return {:db (update db :loading true) :dispatch [:fetch-data]}. The re-frame engine handles the execution of these effects and dispatches the resulting events back into the loop once the asynchronous operation completes.
Trade-offs and Limitations
The re-frame pattern provides immense predictability, but it comes with a structural cost:
- Boilerplate: Even a simple feature requires an event, a subscription, and a view. This can feel verbose for tiny applications.
- Learning Curve: Developers coming from Object-Oriented backgrounds may find the separation of state and logic counterintuitive at first.
- Subscription Chains: If you create deeply nested subscriptions (subscriptions that depend on other subscriptions), it can become difficult to trace why a specific component is re-rendering.
Verification and Testing
To verify your implementation, use the re-frame-devtools browser extension. This allows you to inspect the current db in real-time and see a history of every event dispatched. If a UI element isn't updating, check the reg-sub logic first to ensure the subscription is correctly identifying the change in the state map.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.