Reagent is a minimalistic ClojureScript library for building interactive user interfaces using the React library. It provides a simple, functional approach to frontend development by leveraging ClojureScript's immutable data structures and reactive atom-based state management. Reagent allows developers to define UI components as plain ClojureScript functions that return Hiccup-style data structures, which are then compiled to React elements. Its lightweight design and tight integration with ClojureScript make it popular for building single-page applications and real-time dashboards.
1.1 History and Motivation
Reagent was created by David Nolen and later maintained by a community of contributors. It emerged from the desire to bring React’s component model to ClojureScript while embracing functional programming idioms. Before Reagent, ClojureScript developers often used direct React wrappers that felt cumbersome in a functional context. Reagent’s motivation was to provide a minimal, idiomatic ClojureScript interface to React, reducing boilerplate and making state management reactive by default.
1.2 Relationship with React
Reagent is not a React replacement but a thin wrapper. Under the hood, every Reagent component is compiled into a React component. The library handles the conversion of Hiccup data structures into React elements and manages re-rendering efficiently. Developers can use any existing React library from JavaScript, and Reagent components can be embedded in React projects and vice versa.
1.3 Key Design Principles
1.3.1 Simplicity and Minimalism
Reagent aims to have a small API surface. The core library consists of a few key functions: reagent.core/atom, reagent.core/reaction, and reagent.dom/render. There are no complex class hierarchies or lifecycle hooks—components are regular functions. This minimalism makes Reagent easy to learn and reason about.
1.3.2 Functional Reactive Programming
Reagent adopts a functional reactive style: state changes are propagated automatically to dependent components. Developers define UI as pure functions of reactive state (atoms and reactions). This approach eliminates manual DOM updates and reduces bugs caused by mutable state.
2.1 Reagent Atoms
2.1.1 Creating Atoms
A Reagent atom is similar to a Clojure atom but with added reactivity. Created with (reagent.core/atom value), it holds mutable state. Changes to the atom trigger re-renders of any component that dereferences it. Example:
(def count (reagent/atom 0))
(swap! count inc) ; triggers re-render if count is used in a component
2.1.2 Tracking State with Reactions
Reactions are derived values that automatically recompute when their dependencies change. Created with (reagent.core/reaction [fn]), they return a derefable value. Reactions are lazy and cached, avoiding unnecessary recalculations. For instance:
(def doubled (reagent/reaction [@count * 2]))
2.2 Hiccup Syntax
2.2.1 HTML Elements and Attributes
Hiccup is a vector-based representation of HTML. Tags are keywords or symbols, attributes are maps with keyword keys, and children are subsequent elements. Example:
[:div {:class "container"} "Hello"]
Attributes use kebab-case (e.g., :on-click instead of onClick).
2.2.2 Component Functions
Components are functions that return Hiccup vectors. They receive props as arguments and can use state via atoms. A simple component:
(defn hello [name]
[:h1 "Hello, " name])
2.2.3 Fragment and nil Handling
Reagent supports React fragments via the :<> keyword. Returning nil or false from a component renders nothing, enabling conditional rendering without extra wrappers.
2.3 Reactivity System
2.3.1 Automatic Re-rendering
When a component dereferences a Reagent atom or reaction, the library tracks that dependency. Any change to the atom causes the component to re-render. This is transparent—no manual subscription or cleanup is needed.
2.3.2 Reaction Chains and Deref
Reactions can form chains: a reaction can dereference other reactions. The system detects cycles and avoids infinite loops. Dereffing (@) is the primary mechanism to access reactive values inside components and reactions.
3.1 Stateless Components
3.1.1 Pure Functional Components
Stateless components are plain ClojureScript functions that take props and return Hiccup. They have no internal state and re-render only when their arguments change (due to parent re-render). Example:
(defn greeting [props]
[:div "Welcome, " (:name props)])
3.1.2 Props and Destructuring
Props are passed as a map. Common practice is destructuring in the parameter list:
(defn user-card [{:keys [name email]}]
[:div
[:h2 name]
[:p email]])
3.2 Stateful Components
3.2.1 Using local atoms
Stateful components use reagent/atom for local state. The atom is created inside the component function using (let [local (reagent/atom initial)] ...). Because the component function is called on each render, Reagent provides special handling: atoms created in the top-level of a component are stable across re-renders. Example:
(defn counter []
(let [count (reagent/atom 0)]
(fn []
[:div
"Count: " @count
[:button {:on-click #(swap! count inc)} "Inc"]])))
3.2.2 Lifecycle Methods (with-let, create-class)
For more control, Reagent offers reagent.core/with-let (a macro) and reagent.core/create-class. with-let allows specifying a destructor when the component unmounts. create-class provides explicit lifecycle callbacks like :component-did-mount. Example of with-let:
(defn timer []
(reagent/with-let [tick (reagent/atom 0)]
(js/setInterval #(swap! tick inc) 1000)
[:div @tick]
(finally (js/clearInterval tick))))
3.3 Component Composition
3.3.1 Nesting and Passing Children
Components can be nested by calling them within Hiccup vectors. Children can be passed as extra arguments or via :children in props. Example:
(defn layout [& body]
[:div.container body])
3.3.2 Higher-Order Components
A higher-order component (HOC) is a function that takes a component and returns a new component. In Reagent, this can be done with a function that wraps the original. Example:
(defn with-logging [wrapped]
(fn [props]
(println "Rendering" (:name props))
[wrapped props]))
4.1 Cursors and Derived Data
4.1.1 Driving Subcomponents with Cursors
A cursor is a reference to a nested part of an atom, created with (reagent.core/cursor atom path). Changes to the cursor update the parent atom and vice versa. Cursors allow subcomponents to operate on a slice of state without accessing the whole atom.
(def app-state (reagent/atom {:user {:name "Alice" :age 30}}))
(def user-cursor (reagent/cursor app-state [:user]))
4.1.2 Computed Values via Reagent Forms
Reagent provides reagent.core/form for handling form inputs with two-way binding. (reagent.core/form atom) returns a map with :value and :on-change keys suitable for input elements.
4.2 Interoperability with React
4.2.1 Using React Libraries (e.g., Material-UI)
JavaScript React components can be used via Interop. Import the library and wrap components in Reagent’s reagent.core/adapt-react-class (for classes) or reagent.core/create-class with the :component-name option. Example:
(def Button (reagent/adapt-react-class js/MaterialUI.Button))
[Button {:variant "contained"} "Click"]
4.2.2 Reagent Components in Existing React Apps
To embed a Reagent component in a React app, use reagent.dom/render to mount it on a DOM element. Alternatively, create a wrapper React component that uses Reagent internally via reagent.dom/render on a mount point within the React component’s DOM.
4.3 Routing and Navigation
4.3.1 Simple URL-Driven State
A common approach is to store the current route in a Reagent atom and update it on URL changes using goog.history.History. Components react to the route atom and render accordingly.
4.3.2 Integration with Secretary or Bidi
Libraries like Secretary (route matching) and Bidi (URL routing) integrate naturally. Router functions can update a Reagent atom representing the application state, and components re-render automatically.
5.1 Unit Testing Components
5.1.1 Using cljs.test and reagent.dom
Components can be tested by rendering them into a detached DOM node using reagent.dom/render. Test assertions can inspect the rendered DOM via ClojureScript’s DOM API or use snapshot testing. Example:
(deftest test-greeting
(let [node (js/document.createElement "div")]
(reagent.dom/render [greeting {:name "Test"}] node)
(is (re-find #"Test" (.-innerHTML node)))))
5.2 Debugging Reactive State
5.2.1 Inspecting Atoms and Reactions
Reagent atoms can be inspected with standard ClojureScript tools like prn. Wrapping components with reagent.core/track or adding logging inside reactions helps trace state changes. Browser developer tools can be used to log reactive updates via reagent.core/ratom (plain atoms) with watchers.
6.1 Common Libraries (re-frame, re-com, etc.)
- re-frame: A framework built on Reagent that adds event-driven state management, similar to Redux.
- re-com: A library of reusable UI components (buttons, inputs, datepickers) built with Reagent.
- clj-holmes: A linter for Clojure(Script) that supports Reagent patterns.
6.2 Build Tools (Leiningen, shadow-cljs)
Leiningen is the original Clojure build tool, but shadow-cljs is preferred for modern Reagent projects due to faster compilation, npm integration, and easier configuration. shadow-cljs provides a development server with hot reloading specifically designed for ClojureScript and React.
6.3 Performance Optimizations
6.3.1 Memoization and shouldComponentUpdate
Reagent does not use React’s shouldComponentUpdate by default. To prevent unnecessary re-renders, use reagent.core/with-let with stable children or wrap components with reagent.core/memo (similar to React.memo). Explicitly control re-renders by comparing props.
6.3.2 Avoiding Unnecessary Re-renders
Common techniques include:
- Structuring state so that only affected parts change (using cursors).
- Avoiding large vectors in components; use
forwith keys. - Using
r/partialto create stable callbacks. - Debouncing rapid state updates.
7.1 Simple Todo Application
A typical Todo app uses a Reagent atom holding a list of todo items. Components include an input field (with form binding), a list view, and delete/toggle buttons. All state changes propagate reactively, demonstrating core concepts like atoms, Hiccup, and component composition.
7.2 Real-Time Dashboard
A dashboard subscribes to a WebSocket stream and updates a central atom with incoming data. Reagent’s reactivity ensures that charts (rendered with a React chart library) update automatically. The dashboard uses reactions to compute aggregates and cursors to isolate widget state.
7.3 Integration with WebSockets
WebSocket messages are parsed and used to modify a Reagent atom. The connection lifecycle is managed with with-let for cleanup. This pattern is common in live analytics, chat applications, and collaborative editors.
8.1 Official Documentation and Guides
The official Reagent website (reagent-project.github.io) provides a complete API reference, a getting-started tutorial, and recipes. The source code is hosted on GitHub with extensive examples.
8.2 Popular Tutorials and Books
- *A Brief Introduction to Reagent* (by David Nolen)
- *ClojureScript: Up and Running* (O'Reilly, includes Reagent chapter)
- *Reagent Cookbook* (community recipes)
- Online courses on Pluralsight and Udemy cover Reagent in depth.
8.3 Community Forums and Chat Rooms
The ClojureScript Slack channel (#reagent) is active. The ClojureScript subreddit and Stack Overflow tag reagent provide Q&A. The Clojurians Zulip also has a dedicated Reagent stream.