Migrating from Reagent¶
This page covers the view-layer migration from Reagent to Fresco: component definitions, Hiccup differences, local state, and React interop.
It assumes the application already uses re-frame2 events and subscriptions. If it still uses re-frame v1 shapes, complete that migration first.
Use three migration tools in this order:
- Reporter — classify every foreign React crossing, and census every rostered view-substrate API call site, as mechanical rewrites, human decisions, and runtime blockers.
- Shadow comparison — run the Reagent original and Fresco port side by side and compare canonical DOM and intent streams.
- Codemod — apply only source transformations whose behaviour is decidable from the code.
Do not start with the codemod. Read the report, port and prove a screen, then apply mechanical edits and run the proof again.
1. Generate the migration report¶
The reporter runs on a JVM without loading the application. Its default mode changes no files:
clojure -Srepro \
-Sdeps '{:deps {day8/re-frame2-fresco-codemod
{:git/url "https://github.com/day8/re-frame2.git"
:git/sha "8b17cc53d517de9359f5174a0d2fcfa4748091ab"
:deps/root "migration/reagent-to-fresco/codemod"}}}' \
-M -m re-frame.migration.fresco.codemod path/to/your/src/
Run it from your own project. It needs no checkout of re-frame2: the coordinate
fetches the reporter, and --report out.edn chooses where the report goes.
That coordinate is the reporter's delivery, not a stopgap. The published
Fresco artefact does not carry this tool, so there is no Maven coordinate for
this command to move to later. The :git/sha above is the commit it was last
proved against; pin a newer one whenever you like —
git ls-remote https://github.com/day8/re-frame2.git refs/heads/main prints the
current head, and a SHA is what makes the run reproducible.
Every run writes one EDN report, a scan that changes no source included.
Without --report it goes to reagent-to-fresco-report.edn beside the first
path scanned — point the tool at <repo>/src/ and the report lands at
<repo>/ — and the run prints the absolute path it used.
Reagent converted props crossing through [:>]. Among other behaviours, it
camel-cased nested keys, converted keyword values to names, wrapped
r/partial, and read metadata keys. Fresco does not perform that conversion.
A [:>] form may therefore continue rendering while sending different values
to the component.
The reporter classifies every crossing:
| Category | Named classes | Meaning |
|---|---|---|
| Mechanical | W1–W6, described below | The codemod can preserve the previous behaviour from source text alone |
| Human decision | :computed-props, :computed-value, :computed-nested-key, :adapt-def-site, :cljc-site, :parse-error, :event-carrier-goes-live, :key-conflict, :string-tag-unparseable, :normalized-key-collision, :css-var-repair, :named-ref, :amp-key |
The source does not contain enough information for a safe rewrite, or the change repairs previously broken behaviour that must be reviewed |
| Runtime blocker | :intent-needs-a-declaration, :dangerous-html, :r>-site, :f>-site, :as-element-island, :reagent-api-residue |
The site will raise or silently misrender until someone chooses the correct Fresco shape |
The report is deterministic EDN. Each entry includes:
- file, line, and column;
- the source form as text;
- its classification;
- a recovery sentence;
- the component name where it can be recovered statically.
It is written even when no problematic sites exist and includes the count of untouched sites, so absence from the report is meaningful.
The final suggestions block contains possible h/defhost declarations and
possible callback contracts. Treat them as drafts. A prop named like an event
may actually be a render prop whose return value the library consumes. For
example, declaring a render prop as :event can replace its return value with
a dispatch path and blank the UI without a useful runtime error. Confirm every
contract against the component library's documentation.
The report's second half: the census¶
Everything above describes the fixer, and its population — the [:>]
family — is not what a Reagent codebase is mostly made of. Run over this
repository's own 88-file example corpus the fixer reports zero entries,
because the corpus crosses into React nowhere. A migrator reading only that
sees 88 files the report never mentions.
So the same run also emits a census, under :census, whose population is
the view-substrate API call site: r/atom, r/with-let, r/create-class,
r/as-element, r/cursor, r/reactify-component, root mounting, and the rest
of the two rosters. On that same corpus it reports many, in files the fixer half
never named. No count is quoted here on purpose: the census figure moves whenever
the corpus grows or the rosters widen, and a figure pinned to a commit goes
stale on a change neither of those announces. Run it and read your own.
There are two rosters, 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.
The first is Reagent's API — stock Reagent's namespaces, and the reagent2.*
ones the reagent-slim adapter ships. The second is re-frame2's own substrate
adapters: everything under re-frame.adapter., matched as a prefix anchored at
the start of the namespace, because the adapter set is open and a list of
today's adapters goes stale into the same silent zero tomorrow.
A call site is source that runs. #_(r/atom 0), '(r/atom 0) and
(comment (r/atom 0)) parse into the same nodes a live call does, and the
census prunes all three rather than counting them. A syntax-quote is not
pruned: a macro's template emits real call sites at every expansion.
| Half | Population | Addressed at | Verdicts |
|---|---|---|---|
fixer (:entries) |
[:>]-family crossing sites |
the site | rewrote, or refused in the classes above |
census (:census) |
view-substrate API call sites | the call | :mechanical, :human-decision, :runtime-blocker |
The two halves measure different things and neither is a denominator for the other. §2's translation tables teach some of these shapes and not others; the census carries a recovery note for every class it emits:
| Verdict | Named classes | Meaning |
|---|---|---|
| Human decision | :with-let, :cell-disposal, :outward-bridge, :adapt-react-class, :react-create-element, :props-helper, :reagent-partial, :render-control, :root-mount, :static-markup, :substrate-read-hook, :substrate-view-seam, :substrate-test-seam, :substrate-test-harness |
A Fresco translation exists, but which one depends on intent the source does not carry |
| Runtime blocker | :local-reactive-cell, :derived-cell, :reactive-graph-control, :lifecycle-class, :as-element, :component-introspection |
Fresco has no equivalent tier, so the site raises or silently misrenders until someone chooses the shape |
| Mechanical | none | The bucket is always emitted, at :mechanical 0. Every mechanical rewrite this tool family knows is a W-rule and every W-rule sits at a crossing, so the zero is a measurement rather than an omission |
Two further classes report a resolution failure rather than a translation, both
as runtime blockers. A namespace that spells a Reagent name without being
Reagent's — a vendored inlined copy, for instance — is
:unresolved-reagent-require at the ns form, and every roster-named call in
that file is :unresolved-alias. The tool does not guess that such a copy is
reagent.core, because a wrong binding rewrites working code.
Every legal way of binding the Reagent name is read: an alias, a :refer list,
:refer :all, a :rename, and any of them behind a reader conditional. A
:rename reports under the roster name rather than the local spelling, and it
releases the original — after :refer [atom] :rename {atom ratom}, (ratom 0)
is Reagent's and a bare (atom 0) is clojure.core's.
What it cannot name it does not count, and says so. A Form-2 component is a
defn returning a fn with nothing else marking it, so the census counts the
r/atom that component closes over and reports nothing about the shape itself.
2. Port one screen by hand¶
Migrate a complete screen rather than changing all component declarations, then all handlers, then all crossings across the repository. A screen-level port gives shadow comparison a useful unit.
Two starting points¶
"Reagent" names two codebases here, and they do not migrate the same way.
A Reagent application writes views as defns returning Hiccup, reads with
@(rf/subscribe [:q]), and hands #(rf/dispatch [:x]) closures to callbacks.
That is the left-hand column of the first table below.
A re-frame2 application on the Reagent adapter writes views as rf/reg-view
forms under a [rf/frame-root {...}] wrapper, and reads and dispatches through
the lexical subscribe and dispatch that reg-view injects into every body.
It may call no Reagent API anywhere. The second table below is its column, and
it is the easier of the two ports: the reads and the dispatches are already
data, and only the spellings move.
§1's "absence from the report is meaningful" is a statement about the
populations the report counts. The census reads re-frame2's own
re-frame.adapter. surface as well as Reagent's, but rf/reg-view is
re-frame.core's and sits on neither roster, so a screen written entirely in
reg-view forms can still go unmentioned. Count your own reg-view forms and
work the second table.
The half-migrated tree¶
Porting a screen at a time means the two view layers share one page for the length of the migration. Three facts make that work, and none of them asks for a second root.
One frame serves both halves. Every React-shaped adapter in re-frame2
publishes the frame through one shared React context, and a Fresco boundary
reads that same context. A shell already mounted under [rf/frame-root {:id
...}] — or under [rf/frame-provider {:frame ...}] — therefore supplies the
frame to any Fresco subtree beneath it. A ported screen needs no
h/client-root, no second frame, and no second React root.
Keep the adapter you have. (rf/init! reagent-adapter/adapter) stays as it
is: installing a Reagent, reagent-slim or UIx adapter under a Fresco tree is
supported. re-frame.fresco.substrate/adapter is what lets a finished
application drop its view-library dependency; it is not a prerequisite for
rendering a Fresco view.
The unported shell reaches the ported screen through a bridge. There are two doors, and the choice is about who owns the mount:
(ns app.views.shell
(:require [re-frame.core :as rf]
[re-frame.fresco :as h]
[app.views.feed :as feed])) ;; the ported screen
;; Door 1 — h/as-element, called inside the Reagent body. The props never
;; cross React's prop channel, so they stay ClojureScript values.
(defn root-view []
(case @(rf/subscribe [:app/page])
:feed (h/as-element [feed/feed {:page 0}])
:profile [profile-page] ;; still Reagent
[not-found]))
;; Door 2 — h/as-component, minted ONCE at top level, beside the view it
;; bridges. Reach for it when the Reagent parent must key, mount and
;; re-render the screen as a component.
(def feed-component (h/as-component feed/feed))
(defn root-view-2 []
[:> feed-component {:page 0}])
Minting the component inside a render would allocate a fresh element type on
every pass and remount the subtree, which is React.memo's own law rather than
a Fresco rule.
h/client-root + h/render! (Installation) is the
whole-application door. It is where the migration ends rather than where it
starts: when the last screen is ported, the Reagent root gives way to one
Fresco client-root handle — the same three-name grammar
(client-root / render! / unmount!) the Reagent adapter already
publishes — and the frame-root wrapper is respelled h/frame-root and stays
exactly where it was.
Common translations¶
| Reagent | Fresco |
|---|---|
defn component returning Hiccup |
h/defview, mounted as a Hiccup head |
@(rf/subscribe [:q]) |
(h/sub [:q]), including in branches, loops, and helpers |
#(rf/dispatch [:x]) |
the event vector itself; use h/event when callback arguments matter |
r/atom inside a Form-2 closure |
app-db or the forms module; Fresco has no local-state tier |
r/with-let |
h/reg-state in app-db, or a named native component for DOM-owned state — not a body-local let, which re-runs every render and holds nothing across them |
Form-3 or r/create-class lifecycle |
callback refs or a named native component |
r/track, reaction, r/cursor |
layered subscriptions |
^{:key k} metadata |
:key in the props map |
[:> Component ...] |
remains legal; repair its prop dialect and declare repeated crossings with h/defhost |
r/adapt-react-class |
direct [:>] or a declared host |
r/as-element inside a render prop |
h/as-element inside an h/event at the render prop |
r/reactify-component |
h/as-component, the outward bridge |
The same table for the second starting point. Every row but the last is a spelling change; none of those is a change of shape:
| re-frame2 on the Reagent adapter | Fresco |
|---|---|
rf/reg-view |
h/defview. The view is no longer registered under an id; the var is the head |
subscribe, injected into a reg-view body |
h/sub. A h/defview body binds nothing you did not write |
dispatch, injected into a reg-view body |
the intent vector itself, or h/event when the event matters |
#(do (.preventDefault %) (dispatch [:e])) |
[::h/prevent [:e]] at the same prop |
[rf/route-link {...}], a Hiccup head |
(h/route-link {...}), a plain call |
[rf/frame-root {:id :app ...}] around the tree |
[h/frame-root {:id :app ...}] — the SAME head, the same options, in the same place. A rename, and nothing more |
That last row used to be the exception, and it used to fail silently: the mount
config was closed at three keys and dropped the rest, so a frame-root carrying
:url-bound? or :fx-overrides did not survive being respelled as one. It is an
ordinary row now. h/frame-root passes every rf/make-frame option through
exactly as rf/frame-root does — :url-bound?, :fx-overrides, :images,
:preset and the rest of the record config — and the root door refuses a frame
option rather than ignoring it, so the mistake is loud on the rare occasion
somebody still makes it.
Installation shows the
shape; a routed application needs :url-bound? true, so this is the common path
rather than an edge
(Routing and navigation).
A reg-view body's subscribe and dispatch are lexical bindings the macro
installs. h/defview installs none, so the same source text means something
different under it: a bare rf/subscribe in a Fresco body throws rather than
resolving. Translate every read and every dispatch in a body you move, not only
the ones the compiler complains about.
Two common mistakes fail loudly:
- A Reagent-style
#(rf/dispatch ...)callback has no captured frame when the browser invokes it later, so ambient dispatch raises:rf.error/no-frame-context. Use an intent vector orh/event. - An event vector at an
on*prop of a[:>]crossing now dispatches. Under Reagent it crossed as an inert JavaScript array and never produced a working handler, so the migration turns a dead handler live; decide whether it was ever meant to run, and whether the prop is an event position at all rather than a vendor's on*-named render prop, which needs a:callbacksoverride on a declared host.
Views shared across the boundary¶
In a real application at least one view is rendered by both a ported screen and an unported one — a card, a paginator, an avatar. "Port a complete screen" does not say what happens to it, and duplicating it is the one answer §3 rules out.
Port the shared view once, with the first screen that needs it, and bridge it back out to the callers that are still Reagent:
;; app.views.article-preview — now Fresco
(h/defview article-preview [{:keys [id]}]
(let [article (h/sub [:article id])]
[:article.preview
[:h2 (:title article)]
[:button {:on-click [:article/favourite id]} "Favourite"]]))
;; Minted once, beside the view, for the callers that have not moved.
(def article-preview-component (h/as-component article-preview))
;; app.views.profile — still Reagent, still a defn
(defn profile-page []
[:div.profile
(for [id @(rf/subscribe [:profile/article-ids])]
^{:key id} [:> article-preview-component {:id id}])])
The boundary lands on the view, so there is one implementation and nothing to keep in step. The unported caller changes by one line, and it changes again — back to an ordinary Hiccup head — on the day that screen is ported.
Cross an id, not a value. Props on the h/as-component route travel through
React, so the Reagent parent converts them on the way in exactly as it converts
any other [:>] crossing: a keyword becomes its name, a map becomes a
camel-cased JavaScript object, any other collection is deeply clj->js'd, and
strings, numbers, booleans, nil and functions cross unchanged. Prop names
survive the round trip — :article-id is camel-cased on the way out and read
back as :article-id — but values do not. A ported view handed an id, reading
the rest with h/sub, never meets the conversion at all, and that is the shape
re-frame2 wants anyway.
h/as-element performs no such conversion, because the props stay inside
ClojureScript. Prefer it wherever the Reagent caller is an ordinary body rather
than something that must own the mount.
3. Prove the port with shadow comparison¶
hm/shadow! mounts the original and candidate against isolated copies of the
same seeded frame. One interaction script drives both implementations. At
each checkpoint it compares canonical DOM and the intent stream.
What this step needs first¶
hm/shadow! lives in re-frame.fresco.test.mounted, so it is an L3 door: it
needs the test kit on the classpath and a build target that gives it real React
and a real DOM. Testing carries the kit setup and the level
ladder. A project with no L3 lane has to stand one up before this step, not as
part of it — and if that is more than the screen is worth,
When not to use the full process says so.
The intermediate state¶
Shadow comparison needs the Reagent original still compiling and still mountable, which is a state the rest of this page does not show. For one screen it is three namespaces:
| Namespace | What it holds | Who renders it |
|---|---|---|
app.views.article-row-reagent |
the original, moved verbatim and otherwise untouched | the shadow test, as :reference |
app.views.article-row |
the Fresco port | the shell, and the shadow test as :candidate |
app.views.shell |
the unported shell | the Reagent root |
Move the original into a namespace of its own rather than putting the port beside it under a second name. Every caller then points at one name, the port, and deleting the original at the end is deleting a file.
Point the callers at the port, including the ones that have not been ported themselves — bridging them out is what Views shared across the boundary is for. The original exists for the comparator and for nothing else; a caller left pointing at it is a screen that never migrates.
The comparison¶
(ns app.migration.article-row-shadow
(:require [reagent.core :as r]
[re-frame.fresco.test.mounted :as hm]
[app.views.article-row-reagent :as old]
[app.views.article-row :as new]))
;; Once, at top level, for the same reason h/as-component is: a component
;; allocated per render is a new element type and remounts the subtree.
(def old-article-row (r/reactify-component old/article-row))
(hm/shadow!
{:reference [:> old-article-row {:id 7}]
:candidate [new/article-row {:id 7}]
:initial-events [[:demo/install-fixture]]
:script [{:click "button.edit"}
{:type ["input.title" "Better title"]}
{:click "button.save"}]})
;; => {:status :green :checkpoints 4}
Both sides are mounted by Fresco, so the original arrives the way every
foreign component arrives: through a [:>] crossing or a declared h/defhost,
the same door the translation table above already sends it through. A Reagent
defn written directly in head position is a loud refusal rather than a
Reagent render, which is why :reference is a crossing and :candidate is a
plain Hiccup head.
Two consequences of that crossing decide how the pair is written.
- Cross single-word props. Fresco camel-cases the key on the way out and
a reactified Reagent component reads back the name React actually carried, so
:article-idreaches the original as:articleId. An id both sides agree on — and a seeded frame both sides read — avoids the question and makes the comparison worth taking. - Hand the original its callbacks as intent vectors. A declared callback
contract lowers them into functions closed over that mount's frame, which is
how a foreign original reaches the frame at all. The original's own
#(rf/dispatch ...)closures capture no frame, exactly as §2 says.
Each side receives its own frame copy, so writes cannot leak between the two implementations. A different intent at the first checkpoint causes the states and later DOM to diverge independently, which makes the original cause visible.
A red result identifies the checkpoint and the exact DOM node or intent that differs. When the difference follows a declared policy, such as a Client-only region, the report identifies the policy instead of presenting it as an unexplained DOM mismatch.
A green result means the implementations matched for the flows in the script. It does not prove untested paths. Script the real screen behaviour rather than a single happy click.
Add a sabotage control before trusting the comparator: deliberately change a candidate prop and confirm the run turns red at the expected checkpoint.
Omit :script for interactive development. Both mounts stay live and the call
answers a handle rather than a verdict. No checkpoint is taken
automatically. A reading per committed render is not built, because the
runtime publishes no commit callback, so nothing is compared until you ask for
it. Retain the handle, drive both mounts by hand, call :checkpoint! at each
point you want compared, and :stop! when you are finished.
(let [s (hm/shadow! {:reference [:> old-article-row {:id 7}]
:candidate [new/article-row {:id 7}]})]
;; drive the page by hand, then take a reading
((:checkpoint! s)) ;; => {:status :green :checkpoints 1}
((:stop! s)))
Each :checkpoint! call settles both mounts, compares them, and numbers the
reading; :stop! takes both mounts down.
Shadow comparison covers canonical DOM and intent streams. It does not prove focus, caret, IME, layout, or paint behaviour. Use the browser levels from Testing for those claims.
When the screen is green and its browser tests pass, delete
app.views.article-row-reagent and the shadow test together: the comparator is
the only thing the original was still for, and keeping both copies invites
future divergence. Check first that no caller still points at the original —
a shared view reaches its unported callers through the bridge, not through the
Reagent copy.
4. Apply the mechanical codemod¶
clojure -Srepro \
-Sdeps '{:deps {day8/re-frame2-fresco-codemod
{:git/url "https://github.com/day8/re-frame2.git"
:git/sha "8b17cc53d517de9359f5174a0d2fcfa4748091ab"
:deps/root "migration/reagent-to-fresco/codemod"}}}' \
-M -m re-frame.migration.fresco.codemod --rewrite src/
The same coordinate as step 1, with
--rewrite added. As written it is a dry run; add --write after --rewrite
to write the files.
The codemod uses a lossless parser and preserves formatting, comments, and line endings, including CRLF. A completed run exits 0 even when the report contains human decisions; the tool is a migration assistant, not a permanent build lint.
It applies six rewrite families:
| Rewrite | Input | Output | Behaviour preserved |
|---|---|---|---|
| W1 | ^{:key k} metadata on a vector |
:key k in the props map |
Reagent read metadata; Fresco reads props |
| W2 | Literal nested prop maps | The same map with literal keys camel-cased | Reagent deep-camel-cased nested keys; Fresco passes them by identity |
| W3 | Literal keyword or quoted-symbol prop value | Its name as a string |
Reagent named these values; namespaced keywords lost their namespace there too |
| W4 | Literal (r/partial f a ...) prop |
Hygienic let capture plus function wrapper |
Reagent evaluated the callee and captured args once at construction |
| W5 | [(r/adapt-react-class X) ...] |
[:> X ...] |
Same native React element path in Fresco syntax |
| W6 | [:> "tag" ...] for a plain HTML tag |
[:tag ...] |
Moves the native element onto Fresco's normal, controlled-element path |
A transformation runs only when both old and new behaviour can be determined from the literal source. Event-like prop spelling never authorises a callback rewrite; it can only make the tool more conservative. Everything else remains in the report.
The output of each rewrite is outside that rewrite's input language, so a second run should be byte-for-byte unchanged. Re-run shadow comparison on the screens touched by the diff.
What remains manual¶
Host declarations and callback contracts¶
Only the component library defines whether a prop is an event, plain handler,
render callback, or ReactNode slot. The codemod cannot infer that semantic
contract safely. Review and approve every h/defhost declaration.
Runtime blockers¶
Examples:
:intent-needs-a-declaration: decide whether the event-shaped prop is an event position, or a render prop the vendor namedon*;:dangerous-html: Reagent may have discarded the prop while Fresco will pass it through, turning dead behaviour live;:r>-siteand:f>-site: Fresco can interpret these as unknown tag keywords, so port them explicitly;:as-element-island: a callback runs outside the original render window, so replacingr/as-elementis not a text substitution.
Local state and lifecycle¶
r/atom, cursors, Form-2, and Form-3 structures require a state-ownership
decision. Use the homes in Ephemeral state. No codemod
should decide whether a fact belongs in app-db, a forms address, or native
widget state.
Computed values¶
A map produced by merge, a prop value reached through a symbol, or a key
computed at runtime does not reveal the actual crossing shape to a source-only
tool. The reporter records the site rather than guessing.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
A [:>] site renders but behaves differently |
Reagent converted the prop dialect and Fresco passes values by identity | Run the reporter and apply the safe codemod rewrites |
A former Reagent crossing starts dispatching at an on* prop |
An intent vector that crossed as inert data under Reagent is lowered by Fresco, exactly as on a native tag | Decide whether the handler was ever meant to run; if the prop is a vendor's on*-named render prop, declare the host with {:callbacks {… :render}} |
Callback runs and raises :rf.error/no-frame-context |
A hand-written dispatch closure did not capture a frame | Replace it with an intent vector or h/event |
| A keyed list remounts once immediately after migration | A key collision that Reagent normalised now becomes two distinct values | Accept the one-time transition when the new stable key is correct |
Codemod refuses a nested map with :normalized-key-collision |
Keys such as :foo-bar and :fooBar collapsed onto one Reagent output property |
Remove the unintended duplicate and rerun |
| W2 camel-cases keys in what looks like application data | Reagent already sent that library a camel-cased object | Do not revert unless you intentionally want different library input |
| Shadow comparison is red only in a Client-only region | The difference follows a declared server/client policy | Choose {:server :render}, provide a fallback, or accept the classified difference |
| Shadow is green but focus or IME differs | Shadow comparison does not test browser-only behaviour | Run L4 browser tests |
| A second codemod run changes files | The input changed between runs or another tool edited the output; the codemod itself is idempotent | Compare against the report coordinates and rerun the reporter |
When not to use the full process¶
- For a very small application, run the reporter and port by hand. A shadow harness may cost more than reviewing a handful of screens.
- Keep a React-first screen in raw React or UIx instead of converting it to Hiccup on principle (Islands).
- When a screen is being redesigned, shadow comparison cannot prove intended behavioural change. Spend identity-proof effort on screens that must remain unchanged.
Advanced¶
Report entry shape¶
{:file "src/app/views.cljs"
:line 42
:col 5
:form "[:> Btn {:variant :contained} \"Save\"]"
:head "Btn"
:action :rewrote
:detail {:prop :variant
:was :contained
:now "contained"}
:note "Reagent named every keyword prop value; Fresco keeps the keyword
except at HTML-attribute slots."}
Coordinates always refer to the input file, in report and rewrite modes.
Why W4 captures with let¶
This rewrite is wrong:
It re-evaluates f and @snapshot on each callback invocation. Reagent's
r/partial evaluated the callee and arguments once when the prop was built.
The codemod therefore emits a capture:
[:> Btn {:on-pick (r/partial handler @cart)}]
;; =>
[:> Btn
{:on-pick
(let [f__rf2 handler
a0__rf2 @cart]
(fn [& args__rf2]
(apply f__rf2 a0__rf2 args__rf2)))}]
Names are deterministic and checked against symbols already present at the site. A collision increments the generated suffix for the complete family.
Deliberate divergences left in place¶
The migration tools do not try to remove every semantic difference:
- class collections now coerce and compose under every accepted spelling;
- nested class collections flatten;
- unsafe object keys such as
__proto__,prototype, andconstructorare dropped instead of written; - a literal
nilin a child position remains a child value and React renders nothing for it.