re-frame2 is a specification first and a ClojureScript implementation second
https://day8.github.io/re-frame2/
At a glance
- What is it?
- re-frame2 describes an architectural pattern for single-page apps on a virtual-DOM substrate, and keeps the spec at the root of the repository as the artefact of record while implementation/ follows it. The frames are small virtual machines, effects are data, and the trace bus is a first-class debugging surface.
- Who is it for?
- re-frame2 suits a team willing to treat an architecture document as the source of truth and accept that the shipped ClojureScript is one reading of it rather than the definition. It does not suit a team that wants a library to drop into a React app this afternoon, because the durable value is in the spec and in reading it.
- 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 received new commits within the last day.
- 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 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The spec directory is the artefact, and implementation/ is downstream of it
The central claim of the project is an inversion of how frameworks are normally built. Traditionally somebody writes the implementation, the implementation is the thing, and the documentation tries to describe it, usually failing to some degree.
re-frame2 reverses that. The pattern is defined by the `spec/` directory, and the ClojureScript reference implementation in `implementation/` is a consequence of the spec rather than its source of truth.
The stated goal is a spec complete enough that a sufficiently capable AI can produce a working implementation in one shot. ClojureScript is what ships here, and the README names the other candidates in principle: TypeScript, Melange or ReScript, Fable, PureScript, Scala.js, Kotlin/JS and Squint, described as languages that cross-compile to JavaScript and reach React.
The author's conclusion from that is blunt: if you dislike the specification, change it and generate your own framework from whatever fork pleases you. Value has moved up the chain, code is disposable, and the specification is the durable artefact.
A frame is a small virtual machine and one turn of its pipeline is an epoch
The event model is described in mechanical terms on purpose. A re-frame2 frame is, quite literally, a small virtual machine. Your registered handlers are loaded into it, and they form the instruction set.
Events are the instructions. They arrive from user actions and from other sources, and a frame processes them as an event stream in which every event runs the same pipeline, with no exceptions and no escape hatches.
One turn of that pipeline is called an epoch.
Two consequences follow from having no escape hatches. First, there is no code path in a re-frame2 app that behaves differently in kind from any other, which is what makes the pipeline predictable enough to instrument. Second, the event stream is the program, and the frame is the machine that runs it, which is the vocabulary the rest of the documentation uses.
The project also positions itself as deliberately retro. Back in 2014 React embraced `v = f(s)`, and Redux may have been clunky and incomplete but directionally right, in the author's reading.
Views are derivative, and the argument is aimed at hooks in components
The second claim is a position on causality in view code, and it is argued explicitly against the current React ecosystem.
The premise is a named cognitive bias: what is focal is perceived as causal, so the thing you are looking at gets undue importance. The README argues the React ecosystem has a double dose of it. Components have become increasingly causal: they own state through hooks, they fetch data, they route, they subscribe to stores through a `useFoo` hook somebody plumbed in, and effects sit alongside the view tree. The summary judgement is that this makes a mess.
re-frame2's answer is that views are simple and derivative rather than causal. The Alan Perlis epigraph says it as a warning about the alternative: beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.
The point is not that hooks are bad. It is that an architecture which lets any component reach for anything cannot promise a fixed pipeline, and the epoch model depends on that promise holding.
One trace bus, with redaction policies attached to it
Because the pipeline is predictable, it can be instrumented once. The README's phrase for the result is blunt: your application becomes the ultimate surveillance state.
The mechanism is a single, deeply integrated trace bus. Tools attach to it and see into a running program, and every event leaves an epoch you can scrub through forwards and backwards. That is what makes the model usable with tests, with stories, and with pair-programmer AI tooling that interacts with a running system.
With the ClojureScript implementation you can get a trace form by form, statement by statement, though that is not the default. The default is coarser, and turning the finer granularity on costs something.
The privacy side is handled on the same bus rather than bolted on elsewhere. Policies can elide sensitive values such as auth tokens, and they can elide oversized binary blobs. An observability surface that records everything by default is unusable the moment it meets a production token, so the redaction knobs are part of the tracing design rather than a plugin.
Effects are data, and :rf.http/managed is the one to learn first
The fourth claim is about the world outside your app. An application talks to it constantly over HTTP, websockets, postMessage, socket.io, IPC, push notifications, background workers and server-side fetches during server-side rendering.
The complaint is that every framework treats each of those as its own integration story, with a different retry shape, a different abort dance, a different error taxonomy and a different or absent privacy-redaction story. The result is that the integrations do not compose and the bugs do not transfer.
re-frame2's answer is the managed external effect, one primitive shape every outbound conforms to. Effects are data returned from handlers rather than invoked as callbacks, and the framework owns retry, abort, fan-out, an in-flight registry, teardown, observable trace events, the elision of sensitive and large values, and a structured failure taxonomy under each surface's `:rf.<surface>/*` namespace.
`:rf.http/managed` is the HTTP instance, `:spawn` and `:spawn-all` on state machines are managed effects, and the per-request lifecycle in server-side rendering is another.
A managed WebSocket is deliberately absent, and the pattern fills the gap
The set of shipped surfaces is deliberately small, and the README says so. It does not fold in a managed WebSocket, for instance.
What replaces it is a pattern document. `spec/Pattern-WebSocket.md` shows how to roll your own from the primitives so it composes exactly like the built-ins, and the same is true of the next candidates: postMessage relays, file watchers and Service Worker channels, each inheriting the shape by name.
That is a different distribution strategy from shipping everything. It assumes the reader will follow a pattern document and write twenty lines, and it assumes the failure taxonomy and the trace events matter more than the convenience of a ready-made surface.
The payoff is that the external story composes with the internal one. The same effects-as-data shape covers the nine states of a GUI problem, so the model you learn for HTTP is the model you apply for anything else the application does that leaves the process.
The root package.json lints JavaScript and is forbidden from doing anything else
The root manifest of this repository is one of the more informative files in it. It is named `re-frame2-lint`, it is private, its version is 0.0.0, and its description states the whole policy: it is the repo-root manifest for the JavaScript lint gate only, the ClojureScript build and test toolchain lives in `implementation/package.json`, and nothing else belongs there.
The reason given for putting it at the root is ownership. ESLint lints `.cjs`, `.mjs` and `.js` across `implementation/`, `tools/`, `examples/`, `testbeds/`, `docs/` and `scripts/`, so its manifest has to sit at the root that owns all of them. The same place holds `.clj-kondo/config.edn` and `.splint.edn` for the Clojure trees, which is the parallel arrangement rather than a coincidence.
The dependencies are pinned exactly, `eslint` 10.8.0 and `globals` 17.9.0, and there is a single script, `lint:js` running `eslint .`, configured through `eslint.config.mjs`.
Docs dependencies are pinned with a comment explaining every bump
The documentation site is MkDocs, and `requirements.txt` pins it to the patch level with a comment beside each entry recording why the version is what it is.
`mkdocs==1.6.1` and `pygments==2.20.0` are pinned for reproducibility. `mkdocs-material[imaging]==9.5.44` needs the `imaging` extra because Material's social plugin renders OpenGraph and Twitter cards, which pulls in Pillow and CairoSVG, and CairoSVG in turn needs the Cairo and Pango system libraries that CI installs with apt-get before the pip step runs.
The most interesting entry is `pymdown-extensions==10.21.3`. The comment records that 10.12 fixed the `filename=None` path it passed to the syntax highlighter for untitled fenced code blocks, and that fix is what allowed the `pygments<2.20` ceiling to be retired.
That pattern, a pinned version with the reason attached, is consistent with a repository whose whole argument is that specifications and their provenance matter. `mkdocs.yml` and `mkdocs_hooks.py` sit alongside it, and the tree also carries a `VERSION` file, a `CHANGELOG.md`, a `migration/` directory for the path from the older re-frame, `testbeds/`, `bench/`, `tools/`, `skills/` and a `SKILL-REDIRECT.md`.
Editorial conclusion
re-frame2 suits a team willing to treat an architecture document as the source of truth and accept that the shipped ClojureScript is one reading of it rather than the definition. It does not suit a team that wants a library to drop into a React app this afternoon, because the durable value is in the spec and in reading it. Before adopting it, open spec/Pattern-WebSocket.md and check which surfaces ship as managed effects, since HTTP, spawn and SSR do and WebSockets deliberately do not.
Frequently asked questions
What is a re-frame?
In this repository re-frame2 is an architectural pattern for building single-page apps on a virtual-DOM substrate, React in practice, in which the specification is the artefact and the ClojureScript implementation is downstream of it rather than the source of truth.
What is the relationship between re-frame2 and the original re-frame?
The README describes re-frame2 as the same axe as the earlier re-frame, made from different bits and with new ornamentation. A migration/ directory at the repository root holds the path from the older library.
What is a frame in re-frame2?
A frame is a small virtual machine. Registered handlers are loaded into it as an instruction set, and events from user actions and other sources run through the same pipeline with no exceptions or escape hatches. One turn of that pipeline is called an epoch.
How does re-frame2 handle HTTP and other calls to the outside world?
Through managed external effects. Effects are data returned from handlers rather than callbacks, and the framework owns retry, abort, fan-out, the in-flight registry, teardown, trace events, elision of sensitive and oversized values, and a failure taxonomy under each surface's :rf.<surface>/* namespace.
Does re-frame2 ship a managed WebSocket?
No, deliberately. It ships a focused set of surfaces, and spec/Pattern-WebSocket.md shows how to build one from the primitives so that it composes exactly like the built-ins.
How is the re-frame2 documentation site built?
With MkDocs, pinned exactly: mkdocs 1.6.1, mkdocs-material 9.5.44, pymdown-extensions 10.21.3 and pygments 2.20.0. The imaging extra needs Pillow and CairoSVG, and CairoSVG needs the Cairo and Pango system libraries, which CI installs before the pip step.