Rewrites Reagent VIEW code into **Fresco** (`re-frame.fresco`, alias `h`) — re-frame2's re-frame-native view layer: a view becomes an `h/defview` mounted in brackets, `@(subscribe …)` becomes `(h/sub …)`, handlers become event vectors, Form-2/Form-3 state moves out of the component. **First establish whether the user needs Fresco**: re-frame2's Reagent adapter is first-class and supported, so a v1 app keeps its view code and needs NO rewrite to land on re-frame2 — that is `re-frame-migration`, and it finishes the job. This is an OPTIONAL second step, and Fresco is pre-publication with no released Maven coordinate. **Do not use** for: the v1→v2 events/subs/db migration (`re-frame-migration`), writing new re-frame2 code (`re-frame2`), greenfield setup (`re-frame2-setup`), or live-runtime inspection (`re-frame2-pair`). Trigger on "migrate my Reagent views to Fresco", "port this component to h/defview", or a Reagent view surface (`r/atom`, `@(subscribe …)` in a view) named in a Fresco context.
Resources
8Install
npx skillscat add day8/re-frame2/reagent-migration Install via the SkillsCat registry.
reagent-migration
Helps an author rewrite Reagent view code into Fresco — re-frame.fresco, conventionally aliased h, re-frame2's re-frame-native view layer. A Reagent hiccup view becomes an h/defview mounted in brackets; subscription derefs collapse to (h/sub …); DOM handlers stop being closures and become data the tree retains; and the two things Reagent kept inside the component — local atoms and lifecycle — move to where re-frame can see them.
Read this first — establish that the user needs this at all
This skill is not on anyone's critical path, and the first thing it does is check whether it has a job.
re-frame2 ships first-class, actively-supported adapters. day8/re-frame2-reagent is the default browser substrate and the adapter the reference suite runs against; day8/re-frame2-uix is its peer. An app moving from re-frame v1 to re-frame2 swaps the dependency, installs the adapter with rf/init!, and keeps its view code. That is a finished migration, not a half-finished one, and it is the `re-frame-migration` skill's job.
So rewriting views into Fresco is a separate, optional second step, and it is a rewrite rather than a respelling. Views change shape: parameters become one props map, handlers become data, view-held state leaves the component. Nobody has to take that step to be on re-frame2, and taking it costs real work.
Say the trade plainly and let the author decide. Both columns below are measured against the shipped surface, not the design corpus.
| What the rewrite buys | What it costs |
|---|---|
Handlers are data in the tree. {:on-click [:cart/add id]} — "what does this button do?" is an equality check in a test, with no browser and no click simulation, and a tool can read it. |
Fresco is pre-publication. There is no released Maven coordinate; a project adopts it from source. If yours has no path to that, there is nothing to migrate onto yet. |
h/sub returns the value. No reaction object in application code, nothing to deref, nothing to hold; a read inside a when or a for records an edge only where it happens. |
There is no view-local state tier at all. Every r/atom in a component becomes an ownership decision — app-db, the forms module, or a React island. There is no cell to translate into. |
One boundary model. An h/defview is a real React function component and a legal hiccup head; a plain function in head position is a loud error, not a silent embedding. Reagent's Form-1/2/3 folklore collapses. |
Not every Reagent construct has a home. component-did-update has no mechanism, and Reagent's own component introspection and schedulers assume that renderer's objects. Those views stay on Reagent. |
Foreign React is declared once, and its callbacks need no declaration. h/defhost names a crossing once; each callback's contract is inferred from the prop's spelling exactly as on a native tag, and a one-line :callbacks override covers the vendor whose on*-named prop is really a render prop. |
Some spellings are provisional. Fresco is pre-alpha and names may move; the frame verb it once shipped, h/hframe, is already gone in favour of core's rf/current-frame-id and zero-arity rf/capture-frame, legal inside a body. |
Keystrokes are IME-safe centrally. A key map is composition-gated once, which is the half a hand-written .key test does not have. |
The rewrite is per-view and irreversible in practice. A converted view has no ambient subscribe/dispatch; there is no gradual half-state inside one view. |
When this skill has a job (both halves, or it does not): the author is already on re-frame2, and specifically wants Fresco for some or all of their views. Anything short of both → they stay where they are and you say so.
Never imply the author "should" move. A migration guide that implies the rewrite is necessary is worse than no guide: it costs the reader work they did not have to do.
Start with the reporter — it is a real tool and it runs first
Unlike the view rewrite, the prop-dialect fixer and the view-substrate API census are automated, and they run before you touch anything:
clojure -Srepro \
-Sdeps '{:deps {day8/re-frame2-fresco-codemod
{:git/url "https://github.com/day8/re-frame2.git"
:git/sha "6a5194c0aa029ac1ad34aaf3a62974fd3e5c0221"
:deps/root "migration/reagent-to-fresco/codemod"}}}' \
-M -m re-frame.migration.fresco.codemod path/to/consumer/src/Run it from the consumer's own project — no re-frame2 checkout is needed, and
none is created. The published Fresco artefact does not carry the reporter, so
this coordinate is how the tool is delivered rather than a pre-publication
detour; pin a newer :git/sha fromgit ls-remote https://github.com/day8/re-frame2.git refs/heads/main whenever
you want one.
The reporter reads source text on a bare JVM, loads no re-frame2, and writes a deterministic EDN report with two halves that answer different questions:
- The fixer (
:entries) — every[:> …]-family crossing into React. Reagent converted the prop dialect at those sites and Fresco does not, so a crossing can keep rendering while sending different values. Six rewrite families (W1–W6) are decidable from source text; everything else is a named refusal with a recovery sentence. - The census (
:census) — every rostered view-substrate API call site, across two rosters: Reagent's API (r/atom,r/with-let,r/create-class,r/cursor,r/as-element,r/reactify-component, root mounting, and thereagent2.*namespaces the reagent-slim adapter ships), and re-frame2's own substrate adapters underre-frame.adapter.. The second roster exists because a re-frame2 application on the Reagent adapter calls no Reagent API of its own, and a Reagent-only census scored it at zero. This is the inventory that tells you how big the job actually is, and it is the half a[:>]-only report leaves invisible.
Read the report first. It is exhaustive over what it touched or refused and carries a count of sites left alone, so "not in the report" is unambiguous. Its h/defhost sketches list the callback positions a site uses, and the usual case needs no :callbacks at all — Fresco infers the contract from the spelling. The one thing to check against the library's own documentation is each on*-named prop: a render prop the vendor named on* (Fluent's onRenderCell, Ant's onRow) would infer :event, whose wrapper returns nil and blanks the UI, so that prop gets a {:callbacks {… :render}} override on the host.
--rewrite --write applies only the six decidable families, and it is the LAST step, not the first: port and prove a screen by hand, then apply the mechanical edits. Full detail is in the codemod's own README.
The mental model (read this before touching a view)
Reagent runs a view at render time as an ordinary function that returns hiccup. Fresco's h/defview mints a real React function component which is a legal hiccup head and nothing else. Four shifts follow, and internalising them is most of the migration → `references/mental-model.md`:
- Brackets mount, parens inline.
[todo-row {:id id}]mounts a boundary that owns its own subscription edges and memoisation;(row-bits id)is an ordinarydefnhelper running inside whoever called it. Changing brackets to parens changes ownership, not spelling. - Deref-drop. A subscription is read with
(h/sub [:q]), not@(subscribe [:q]). It returns the value — there is no reaction object. The read is ambient: legal inside awhen, afor, or an inlined helper, and a branch not taken contributes no edge. - Handlers become data.
{:on-click #(dispatch [:ev x])}becomes{:on-click [:ev x]}. The shape of the value at anon-*position selects the behaviour — vector, key map,h/event, or plain function — so there is no roster of blessed prop names and:on-clickand:onClickread the same. - The view holds no state. Fresco has no
local, nouse-state, no cell of any kind, and that absence is the design. Product state goes to app-db (h/reg-stateis the sugar); a draft-and-commit control isre-frame.fresco.forms/buffered-field; genuine widget mechanics go to a React island — a UIxdefuior a raw React function component mounted throughh/defhost, withre-frame.fresco.native's two hooksuse-sub/use-framefor Fresco state — where React's own hooks are legal.
Cardinal rules (the invariants)
- The view rewrite is JUDGMENT, not a codemod — but the reporter is a real tool. Run the reporter first (above); it inventories the Reagent surface and fixes the prop dialect at React crossings. No tool converts views: for an ambiguous view the skill reasons about the right shape rather than emitting a flag.
- The whole view is the unit of migration — never half-migrate a view. A converted
h/defviewhas no ambientsubscribe/dispatch, so a body with some sites rewritten and some not does not work. When a view raises a judgment call (D-tier), decide it with the author, then convert the whole view or hold the whole view. When a view needs a surface Fresco does not have (R-tier), hold it on Reagent and say why. - Incremental, never big-bang. Migrate one namespace / one closed subtree at a time; verify it renders and its tests pass; then move on — `references/procedure.md`.
- The MIG rule catalog is the shared vocabulary. Every rewrite cites a
MIG-NNid, so the author can trace any change back to a rule. The id names the Reagent construct you found, not the destination shape. If a construct matches no rule, treat it as a hold (rule 2). - Views only. This skill rewrites the view tier — hiccup, handlers, mounts, view-held state. It never touches events, subs, fx, machines, schemas, or routes (that dataflow is re-frame2 already, from step 1). Where a view forces a dataflow change (a new
reg-sub, a hoisted event), the skill names it for the author — it does not reach across into the dataflow layer. - Emit only what has shipped, and read the door to find out. Fresco's public surface is
re-frame.frescoplus six optional modules (.forms,.motion,.overlay,.native,.server,.substrate) and the test kit (re-frame.fresco.test*—.test,.test.mounted,.test.forms,.test.runtime,.test.server). No guide page is the API: pages have taught forms that do not exist — anh/fnspelling, since swept — and one still overstates the shipped surface today, restricting key maps to:on-key-down/:on-key-upwhen the lowering accepts one anywhere. If a verb is not in the shipped door, do not write it: name the gap and hold the view. - The skill runs the compile/test gates; the programmer owns the visual confirmation. Migration is verify-as-you-go, so the skill discovers and runs the nearest safe noninteractive gate itself — compile the subtree and run its tests (
npx shadow-cljs compile …,npm test,clojure -M:test, whatever the project uses). "Compiles" is necessary but not sufficient: Fresco moves most view errors to run time by design.
The transformation catalog — organised by tier
Split three ways by what you do with the rule, not by construct. Load the tier you need:
- `references/catalog-mechanical.md` — M-tier ("do this"). Unambiguous rewrites with a before→after for each: Form-1 →
h/defview(MIG-01), deref-drop (MIG-02), dispatch-lifting and the two markers (MIG-04/05),preventDefault(MIG-06), key-meta →:keyprop (MIG-07), the prop dialect (MIG-11),doallstrip (MIG-12), plain hiccup pass-through (MIG-14), root mounting (MIG-15), ns requires (MIG-24), keystroke handlers → a key map (MIG-33). Apply these directly. They preserve what the view does — with one divergence to know before you start: the handler-lifting rules (MIG-04/05, MIG-06, MIG-33) also change when the event drains, because a Fresco intent dispatches synchronously whererf/dispatchqueued. `references/mental-model.md` §3 states it once; MIG-04/05 carries the one check and the one escape. - `references/catalog-judgment.md` — D-tier ("here's how to DECIDE"). The cases that earn the skill its keep: view-local state (MIG-16 Form-2/
with-let), lifecycle (MIG-17 Form-3), non-conforming:on-*handlers (MIG-18), derived state (MIG-19), the ratom-as-store restructure (MIG-20), plain-fn ambient reads (MIG-26), fn-valued props on internal views (MIG-27), computed props (MIG-28), foreign React heads and their fn-valued props (MIG-09/10/22), SSR-then-hydrate (MIG-23, whose recipe is its own leaf — below), and the loop / render-prop shaping calls (MIG-08/13). For each: the decision the AI makes, not a flag. - `references/catalog-reject.md` — R-tier ("don't migrate this — stay on Reagent"). The honesty backbone, and it is short: Reagent introspection and schedulers (MIG-35), a frame-pinned reactive read (MIG-03), and
component-did-update's prev-props protocol (MIG-36). - `references/ssr-hydrate.md` — MIG-23's SSR-then-hydrate recipe, and the one leaf a client-only migration never opens: the cold two-process boot condition, the server half, the three ordered client calls, and the
:identifier-prefixcontract. Load it only when a hydrating root is in scope.
The procedure (incremental)
Full loop in `references/procedure.md`. The shape:
- Run the reporter and read both halves. That is the inventory the plan is built on.
- Scope a closed subtree. Convert leaf views first, closing bottom-up so each pass ends renderable and tested. Leaf-first is the clean default, not a wall —
h/as-componentmounts a converted view under a parent staying on Reagent, UIx or plain React when one is unavoidable. - Assess the view first (rule 2). Scan each candidate for D/R hits. An R hit → hold the whole view on Reagent. A D hit → decide it with the author, then convert the whole view or hold the whole view.
- Apply the M-tier rewrites to the clean views, atomically per view (a header change and all its call sites in one edit).
- Fix the ns requires and the root last (MIG-24, MIG-15): add
[re-frame.fresco :as h]; dropreagent.*requires only when nothing in the namespace still needs them. - Compile + test the subtree (the skill runs the gates); the programmer renders + eyeballs it. Only then move to the next.
Gotchas
The traps that mangle a view silently → `references/gotchas.md`. The one to internalise before anything else:
A leftover #(dispatch …) closure compiles, renders, and fails at CLICK time. Fresco passes an unmarked plain function through to React by identity — deliberately, so React.memo and handler-identity bail-outs keep working — so nothing refuses it at render. When the browser invokes it later there is no render extent, ambient dispatch has nothing to resolve, and it raises :rf.error/no-frame-context. That is the whole-view-coherence law (rule 2) with teeth: grep the converted bodies for surviving closures rather than finding them by clicking.
And it is one of three. The same half-conversion fails at three different times under three different ids: a leftover ambient rf/subscribe/rf/dispatch inside the render extent — the body or a helper it inlines — refuses at RENDER with :rf.error/ambient-frame-refused; the closure above fails at CLICK with :rf.error/no-frame-context; an h/sub hoisted out into a callback or timer fails at FIRE with :rf.error/fresco-sub-outside-render. Branch on :rf.error/id and read :reason, which names the fix — the table and the complaint shape are in `references/gotchas.md`.
The rest: the bare-symbol trap ([:li item] — item is content), brackets vs parens, the exactly-one-props-map law, data-vectors-are-not-hiccup ([:buy 1] in an :on-click is an event), markers do not nest (a ::h/value below the vector's top level arrives as a literal keyword), and a key map at :on-submit prevents on every branch.
Done checklist
- The author was told they do not have to do this, and chose to anyway; the app is already on re-frame2 and Fresco is reachable from its build.
- The reporter was run and both halves of its report were read.
- Each converted view is whole — no half-migrated bodies (rule 2), and no surviving
#(dispatch …)closure. - Every rewrite cites its
MIG-NNid so the author can audit it. - The D-tier views were decided with the author, not silently rewritten.
- The R-tier views were left on Reagent with an honest reason.
- Every Fresco verb emitted exists in the shipped door, checked there rather than in a design page (cardinal rule 6).
- Requires cleaned up last (MIG-24); no orphaned
reagent.*requires, none dropped that a held view still needs. - If no Reagent view remains, the author was told the adapter choice is now open —
re-frame.fresco.substrate/adapterretiresday8/re-frame2-reagent, and droppingreagent/reagenttoo is a separate call resting on a whole-repository measurement — the reporter's files that name Reagent count, not its call-site count, plus a textual sweep of dependency config — rather than on the view count, because this skill never read the non-view code (MIG-24) — and made both calls themselves. Not decided for them, and never raised while a Reagent view is still standing. - The subtree compiles and its tests pass (the skill ran the gates), and the programmer has rendered and eyeballed the converted views.
Hand off: "Views rewritten into Fresco where it made sense; the rest stay on Reagent, which is a fully-supported configuration. Switch to re-frame2 for new application code, or re-frame2-pair for live inspection."
Anti-patterns
- Don't run this before the v1→v2 migration. Fresco is a re-frame2 view layer; it presupposes step 1 is done. → `re-frame-migration`.
- Don't sell the rewrite as required. The Reagent and UIx adapters are first-class homes for views indefinitely, and Fresco has no published coordinate yet.
- Don't half-migrate a view (cardinal rule 2) — coherence over coverage.
- Don't auto-spread a bare symbol child (
[:li item]) — it is content, not props. - Don't emit a verb because a guide page names it. The
h/fnspelling that used to be the standing example was swept toh/eventon 2026-08-15, but the class did not go with it: the shipped guide still restricts key maps to:on-key-down/:on-key-upwhen the lowering accepts one at any event position. Check the door. (A plainmergewith the owned keys last IS the shipped spelling for forwarding caller attrs; there is no reserved merge key.) - Don't invent a listener-options map. There is no
{:event […] :prevent-default true}, no:capture, no:passive, no:once, no:stop-propagation— not undocumented, unrepresentable.::h/preventis a reserved head and imperative event work belongs inh/event. - Don't declare
:callbacksfor the usual case, and don't skip the check for the unusual one — the contract is inferred from the spelling, and the override exists for the vendor'son*-named render prop, where the inferred:eventwrapper blanks the UI silently. Check everyon*prop's return value against the library's documentation. - Don't reach into the dataflow layer — name the
reg-sub/event the view needs; let the author write it (cardinal rule 5).
Fresco's public door is `implementation/fresco/src/re_frame/fresco.cljc` — read it, not a design page, for what has shipped. Full skill-routing map: `skills/README.md` §Skill routing.