Skip to content

React Compiler: the Vite plugin's reactCompiler option, "use memo" directives, a react-compiler template - #17

Closed
k7d wants to merge 1 commit into
esm-destructure-arraysfrom
react-compiler-template
Closed

k7d wants to merge 1 commit into
esm-destructure-arraysfrom
react-compiler-template

Conversation

@k7d

@k7d k7d commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Adds a react-compiler template to create-clojurific, and what it needs in Clojurific: the Vite plugin runs React Compiler, and ^:memo fns get React Compiler's "use memo" directive.

Stacking: based on #16 (destructuring) → main, which has #13 (#x literals), #14 (const/let) and #21 (munged-name collisions). #15's for change is included here (see below).

Changes

  • Vite plugin, reactCompiler option (vite-plugin-cljs/react-compiler.js):

    • Runs babel-plugin-react-compiler (with @babel/core, both installed in the project) on the compiled namespaces, with source maps.
    • true or React Compiler's own options. By default (compilationMode: 'annotation') only modules with ^:memo fns ("use memo" functions) are transformed.
    • It passes React Compiler an environment.moduleTypeProvider declaring the JSX runtimes (react/jsx-runtime, preact/..., hono/jsx/...) and cljf.jsx's compiled module the way React Compiler declares React.createElement: arguments frozen, frozen elements returned. Without that, React Compiler treats jsx() as an unknown, possibly mutating call and caches nothing apart; with it, it memoizes per element as it does JSX syntax. A user's own moduleTypeProvider is consulted first.
  • Compiler, ^:memo: in ES module output, a ^:memo fn starts with React Compiler's "use memo" directive: (defn ^:memo App [] ...) or (fn ^:memo Row [props] ...). It's read from the name's metadata, as ^:async is. Fns nested in it aren't marked. Components and hooks are fns of one fixed arity, so a multi-arity or variadic ^:memo defn gets a :memo-fn warning and no directive. Classic output ignores ^:memo.

  • for in ^:memo fns (from ES module output: for's iterators call themselves through a letfn local #15, closed): in a ^:memo fn, for's iterators are bound with letfn* to an anonymous fn and call themselves through that local. A named function expression calling itself makes React Compiler reject the component ("Expected value kind to be initialized"). ^:memo marks the analyzer env :memo, which the for macro reads. Outside ^:memo fns, for is unchanged. In letfn*, an anonymous fn now refers to itself through its binding (before, the self-call resolved to a namespace var that doesn't exist); letfn's own self-named fns are unchanged.

  • React's array hooks destructured by index (moved here from ES module output: destructure #js [] arrays by index #16): cljs.analyzer/js-array-init? also counts the values of react's (and preact/hooks', preact/compat's) useState, useReducer, useTransition, useActionState, useOptimistic and useFormState. Their [value setter] is read with (v?.[i] ?? null) instead of nth, whose call React Compiler treats as mutating the array: in a two-useState component it memoized nothing (0 slots) without this, and 10 slots with it. The check is by module and fn, so another module's useState goes through nth, as do useMemo's values (any value) and loop locals.

  • Template react-compiler:

    • reactCompiler: true in vite.config.ts;
    • babel-plugin-react-compiler and @babel/core as dev dependencies;
    • ^:memo App and Row components, built with #x, for and useState.

    It's in the interactive menu ("React + React Compiler"), the create-clojurific README and its tests, and in script/test-launcher, which also checks that the bundle has React Compiler's memo cache.

Verified

  • Dev server (scaffolded project, the plugin linked from this checkout):
    • The served module is React Compiler output.
    • Clicking the counter re-renders App; the rows' React props objects stay identical across the clicks, so the Rows aren't re-rendered.
    • Adding a row keeps the existing rows' props too.
  • vite build + vite preview: the bundle contains the memo cache and the app works.
  • Earlier browser test, the same pattern counting Row renders: 15 re-renders after 5 unrelated state changes without React Compiler, 0 with it.

Tests

  • script/test-esm: 0 failures, 0 errors. A new test covers ^:memo on defn and named fns, fns nested in them (not marked), a string in the body (not a directive), and the warning for a multi-arity ^:memo fn.
  • Compiler tests: 0 failures, 0 errors.
  • node --test (launcher, plugin, create-clojurific): 69 pass, 1 skipped. The new React Compiler transform tests run with CLJF_REACT_COMPILER_DIR pointing at a project that has the Babel packages installed, and are skipped otherwise. They pass when run that way.
  • Not run: the full script/test-launcher.

Notes

  • React's rules apply. A component reading an atom during render (as the first draft of the template did, to count renders) is memoized as if it read nothing, so it doesn't update.

@k7d

k7d commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Merged the review fixes from #14, #15 and #16 in. The ^:memo directive is written before the hoisted locals' placeholder, so nested fns don't copy bodies. In ^:memo fns, for uses the letfn iterators React Compiler accepts.

I re-checked the template in the dev server: the module is React Compiler output, it destructures useState with ?.[i] ?? null, and the rows keep their props across counter clicks. script/test-esm, the compiler tests and the Node tests pass.

@k7d

k7d commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Restacked on main with #21 merged (312dfa40), force-pushed:

The review-fix merges are folded into the branches' own commits. The code is unchanged apart from #21's changes. Only CHANGELOG.md and the build tests needed conflicts resolved, and each branch's [Next] section now has main's entries plus its own.

On every branch, script/test-esm and the compiler tests pass, including #21's new test-locals-munged-to-the-same-name. On #17 the Node tests pass too, and the react-compiler template works in the dev server (memoized rows).

…^:memo fns with its "use memo" directive (for's iterators call themselves through a letfn local in them), React's array hooks' values destructured by index, a react-compiler template
@k7d
k7d force-pushed the esm-destructure-arrays branch from 458872c to c97bad1 Compare October 30, 2026 13:05
@k7d
k7d force-pushed the react-compiler-template branch from 90d812f to 68faeb1 Compare October 30, 2026 13:05
@k7d

k7d commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

putting this on hold for now for further consideration

@k7d k7d closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant