The React Compiler is enabled for the whole app (babel({ presets: [reactCompilerPreset()] }) in vite.config.ts). It auto-memoizes every component and hook at build time, so hand-written memoization is noise — it adds clutter without improving performance.
- Don't write
useMemo,useCallback, ormemo()— the compiler handles it. - Three exceptions exist (dep-array functions, hot-list
React.memo, lint escape hatches); add a comment on each. - Enforcement gate:
eslint-plugin-react-compilerinbun run lint/ CI. react-doctorsurfaces violations as warnings only (react-doctor.config.json); it cannot distinguish the three legitimate exceptions.
- No
useMemo, nouseCallback, nomemo(). Write the plain value, the plain function, the plain component — the compiler memoizes them for you. New code must not introduce manual memoization, and existing manual memoization is being removed. useState(() => …)lazy initializers anduseRef(…)are NOT memoization — always fine, unaffected by this rule.
Memoization legitimately stays in exactly three cases. Keep it, and add a one-line comment saying why so the next reader (and the linters) know it's deliberate.
-
The value/function is referenced in a hook dependency array. The static
react-hooks/exhaustive-depsrule can't see the compiler's runtime memoization, so it still demands a stable identity for anything in auseEffect/useMemo/useCallbackdep array. Wrapping a function used as a dep inuseCallback(plus the transitive closure it depends on) is required to keepbun run lintclean. Only functions trip the rule — a plain value feeding a dep array can be inlined. -
A
React.memore-render bailout on a hot, list-rendered component (e.g. a recursive per-node canvas/tree renderer rendered O(N) times).React.memoskips re-rendering on equal props — a different mechanism from the compiler's within-component memoization — so dropping it on an O(N) critical path is not behavior-preserving without runtime perf validation. Rare; justify in a comment. Examples:NodeRenderer,DomPanel/TreeNode,AgentPanel'sMessageBubble/MarkdownTextBubble. -
A lint escape hatch the compiler/linters force. Two sub-cases:
react-hooks/refs: a render-scoped event handler that reads/writes a ref (someRef.current = …) trips "Cannot access refs during render" when written as a bare function, because the linter can't tell the closure only runs at event time. Wrapping it inuseCallbacksatisfies the rule. (SeeCanvasLiveSurface's pointer handlers.)- Compiler bail-out: when the compiler genuinely cannot compile a function, add the
"use no memo"directive (or the existingeslint-disable react-compiler/react-compilerpattern) and keep the manual memoization it needs.
Enforcement is eslint-plugin-react-compiler + eslint-plugin-react-hooks, run in bun run lint / CI — that is the authoritative gate:
eslint-plugin-react-compilerflags functions the compiler had to bail out on.react-hooks/exhaustive-depsandreact-hooks/refsenforce exceptions (1) and (3).
react-doctor's react-compiler-no-manual-memoization rule also flags manual memoization, but it cannot recognize the three exceptions above, so it false-positives on them. It is therefore configured as an advisory warning (react-doctor.config.json), not an error gate — it surfaces genuinely-gratuitous memoization on new code without blocking on the legitimate exceptions. Treat a new useMemo/useCallback/memo() outside the three exceptions as drift and remove it.
CLAUDE.md→ "React Compiler and memoization" — the rule summary with direct agent instructionsvite.config.ts— compiler setup (reactCompilerPreset)eslint.config.js—eslint-plugin-react-compilerandreact-hooksconfigurationreact-doctor.config.json— advisory downgrade forreact-compiler-no-manual-memoization