Library / SDK
cjohansen/replicant avatar
cjohansen/replicant

replicant: a Clojure renderer with no components and no dependencies

A data-driven rendering library for Clojure(Script) that renders hiccup to DOM or to strings.

537 stars25 forksClojureMIT

At a glance

What is it?
replicant renders hiccup to DOM nodes or to strings, and its design goal is that the whole interface is a pure function of application state, re-rendered on every change. It deliberately has no component system, no local state and no dependencies at all, which is the source of both its simplicity and its limits.
Who is it for?
replicant fits a Clojure team that wants a view layer with no dependency tree, no component abstraction to learn and no runtime surprises, and that is content to re-render the whole tree on every state change. It is the wrong tool if you need component-local state, a large third-party component ecosystem, or interoperability with React components, since the author rules both out deliberately.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 59 days ago.
What is it written in?
Mainly Clojure, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One function, called again and again

replicant is described as a data-driven rendering library for Clojure(Script) that renders hiccup to DOM or to strings, over and over, efficiently, without a single dependency. That last clause is literal. The install coordinates name one artefact and no transitive libraries appear in the picture:

clj
no.cjohansen/replicant {:mvn/version "2026.07.1"}

The usage is deliberately dull. You write your interface as hiccup, pass it to the renderer, and you get DOM nodes back:

clj
(require '[replicant.dom :as r])

(r/render js/document.body
  [:div.media
   [:aside.media-thumb
    [:img.rounded-lg {:src "/images/christian.jpg"}]]
   [:main.grow
    [:h2.font-bold "Christian Johansen"]
    [:p "Just wrote some documentation for Replicant."]
    [:p.opacity-50
     "Posted February 26th 2025"]]])

Or you render to a string, on the client or on the JVM, which is the server-side rendering path:

clj
(require '[replicant.string :as s])

(s/render
  [:div.media
   [:aside.media-thumb
    [:img.rounded-lg {:src "/images/christian.jpg"}]]
   [:main.grow
    [:h2.font-bold "Christian Johansen"]
    [:p "Just wrote some documentation for Replicant."]
    [:p.opacity-50
     "Posted February 26th 2025"]]])

The same hiccup goes in both cases. That is the point of the library, and it is what makes it usable from a Clojure backend that generates markup for a page that a ClojureScript client then hydrates.

The design goal is deliberately narrow: state in, hiccup out

The README states the model precisely. The user interface is a function of application state, and whenever state changes you call this function to update the UI. The function receives all application state and returns the entire user interface as hiccup data, every time. The summary is state, which is data, in, hiccup, which is data, out, and then the list of what is not there: no local state, no atoms, no subscriptions, no networking. Just a pure function from data to hiccup. The library is then scoped to match that goal. There is no state management, no async rendering and no networking utilities, only a single function that renders and rerenders the hiccup to the DOM in an efficient manner. Everything else in the README is either an explanation of that choice or a careful note about the edges of it.

No components, on purpose, and that is a design decision not an omission

The author explains why components are absent. Snabbdom taught him that components are not a necessary feature of a virtual DOM renderer, and that life-cycle hooks can just as easily be attached to DOM nodes. The README says Replicant was very deliberately designed to not include certain features popular in other rendering libraries, such as component local state, or components for that matter, and it gives the reason: enabling Replicant to render components written for other libraries would effectively introduce an escape hatch that would undermine Replicant's assumptions. Given the stated view that reusable components across libraries and frameworks are not an attractive goal, this is not complexity the author is interested in taking on. The practical effect is that a Replicant application is a single pure function, and the cost of that is that every piece of reusable view logic has to live outside the library, in your own namespaces.

Interoperability with the browser, but not with React

replicant interoperates with anything that works on data, and it has hooks that expose DOM elements so other libraries can be brought in. Interoperability with component libraries like React is technically possible through hooks, but the README says it is not recommended, on the grounds that virtual DOM libraries and frameworks are not lightweight enough to encourage using more than one in the same application. The recommended path for reusable browser extensions is web components, and Replicant renders custom elements directly, so the custom element tag is just another hiccup tag:

clj
[:u-tabs
 [:u-tablist
  [:u-tab "Tab 1"]
  [:u-tab "Tab 2"]
  [:u-tab "Tab 3"]]
 [:u-tabpanel "Panel 1"]
 [:u-tabpanel "Panel 2"]
 [:u-tabpanel "Panel 3"]]

That approach keeps the escape hatch outside the library, which is what the design intends. The custom element owns its own behaviour and lifecycle, and Replicant treats it as an opaque node it can create, update and remove.

The changelog shows the maintenance is about edge cases, not features

The most recent release, 2026.07.1, fixes an SVG namespace leak on foreignObject children during reconciliation and adds support for using the library with cherry. The release before it, 2026.06.2, added support for squint, and 2026.06.1 contains a list of fixes that reads like a bug report from real use: calling unmount hooks on the children of unmounting nodes, recovering after an exception during rendering, not tripping on top-level lists with nils in them, accepting any seq at the top-level and not just lists, and properly escaping attribute values when rendering to strings. Earlier, 2025.12.1 fixed the ordering of the value attribute so that range inputs do not clamp their value, and added support for rendering seqs of hiccup. The pattern is consistent. The API is stable and the work is in the reconciliation details where real applications hit real DOM behaviour.

Running the tests, and the build targets behind them

The repository carries several build configurations because it targets several runtimes. The README asks that bug fixes come with test cases demonstrating the problem, and that performance improvements come with some sort of numbers demonstrating, and gives the test command:

sh
clojure -X:dev:test

The Makefile shows what sits behind that. There is a `test` target running kaocha against the Clojure sources, a `test-cljs` target for the ClojureScript side that depends on `node_modules`, and a `test-compile` target that produces a shadow-cljs release build. The npm side of the project has its own script, `squint compile && node lib/replicant/run_tests.mjs`, with squint-cljs and cherry-cljs as dev dependencies, which is how the squint and cherry integrations are covered. There is a deploy target that builds a jar and pushes it to Clojars, and the presence of pom.xml, deps.edn, bb.edn, shadow-cljs.edn and squint.edn in the top level reflects the same fact: this library is expected to work across Clojure, ClojureScript, babashka and the various ClojureScript compilers.

Where replicant is the wrong tool

Three limits are concrete and all of them are consequences of the design goal. The first is that rendering the entire interface on every state change is fine for a modest tree and expensive for a large one, so an application with a big list and frequent updates will spend time in reconciliation that a component-based library would avoid through local state. The README points at the js-framework-benchmark suite as the way performance is being tuned and mentions benchmarking.md for running it locally, but that is a tuning effort rather than an architectural answer. The second is the ecosystem. There is no component library to reach for, so a team that needs a rich date picker, a virtualised table or a rich text editor has to find a web component or write one. The third is interoperability with React, which the author says is technically possible through hooks but not recommended. There is also a project-level signal worth reading before you invest: the README states plainly that the author will not review the results of LLM prompts and expects pull request authors to understand and vouch for the code they submit.

The alternative is a full framework, and the difference is what you give up

The realistic alternative on the JVM is a ClojureScript framework such as Re-frame, which adds a component abstraction and its own state handling on top of rendering. The difference in approach is where the re-render boundary sits. With a framework, state changes are tracked and only the components whose inputs changed re-render, which is what makes a large interface tractable. With replicant, you hand over the whole tree every time and the library works out the minimal DOM operations, so the rendering cost is proportional to the size of the tree rather than to the size of the change. What you get in exchange is a dependency-free library with a single function and no framework conventions to learn, no atom discipline to maintain, and a rendering model you can reason about because it is a plain function call. The other alternative is writing the DOM manipulation yourself, which is essentially what replicant does for you, and that comparison tells you how much work the library is actually doing.

Editorial conclusion

replicant fits a Clojure team that wants a view layer with no dependency tree, no component abstraction to learn and no runtime surprises, and that is content to re-render the whole tree on every state change. It is the wrong tool if you need component-local state, a large third-party component ecosystem, or interoperability with React components, since the author rules both out deliberately. Before you adopt it, read the user guide at replicant.fun/learn, check that the release you pin matches your ClojureScript build, and confirm the public API stability claim holds for the version you choose, since the repository publishes no GitHub releases.

Frequently asked questions

How do I add replicant to a Clojure project?

Add the dependency `no.cjohansen/replicant {:mvn/version "2026.07.1"}` to your project. The library has no dependencies of its own.

Can replicant render to a string as well as to the DOM?

Yes. The `replicant.string` namespace renders to a string on the client or on the JVM, and `replicant.dom` renders to DOM nodes, with the same hiccup input in both cases.

Does replicant have components or local state?

No, and that is deliberate. The README says Replicant was very deliberately designed to not include component local state, or components for that matter, and that the library is only a rendering library with no state management, no async rendering and no networking utilities.

How do I run the replicant test suite?

Run `clojure -X:dev:test`. The Makefile also has separate targets, with `test` running kaocha for the Clojure sources and `test-cljs` covering the ClojureScript side through a shadow-cljs release build.

Official sources

  1. cjohansen/replicant on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cjohansen-replicant.svg)](https://hysenlabs.com/projects/cjohansen-replicant)

Was this useful?