|
| 1 | +--- |
| 2 | +description: Rely on the React Compiler (`'use memo'`) instead of manual `useMemo` / `useCallback` |
| 3 | +paths: |
| 4 | + - "react/**/*.{tsx,ts}" |
| 5 | + - "packages/backend.ai-ui/**/*.{tsx,ts}" |
| 6 | +--- |
| 7 | + |
| 8 | +# React Compiler Memoization Rule |
| 9 | + |
| 10 | +This project runs **React 19.2** with **`babel-plugin-react-compiler` in annotation mode**. The compiler owns memoization. Add the `'use memo'` directive to each component and hook body, and write plain values and plain functions. |
| 11 | + |
| 12 | +## Why |
| 13 | + |
| 14 | +- Manual `useMemo` / `useCallback` dependency arrays drift out of sync as code evolves; the compiler's view is always current with the code it compiles. |
| 15 | +- `useCallback` forces every identity-sensitive caller (child components, other hooks) to also memoize. The compiler short-circuits this cascade. |
| 16 | +- Consistent adoption keeps the memoization story legible across the codebase — reviewers don't decide case-by-case whether a given `useCallback` is load-bearing or ceremonial. |
| 17 | + |
| 18 | +## Rules |
| 19 | + |
| 20 | +1. **Add `'use memo'` at the top of every component body and every custom hook body** that would benefit from memoization. This is almost all of them. Never remove an existing `'use memo'` directive. |
| 21 | +2. **Do not introduce `useMemo` or `useCallback`** in new code. Replace existing usages with plain values and plain functions when you touch them, and add `'use memo'` if the enclosing function doesn't already have it. |
| 22 | +3. **Callbacks passed to JSX** (`onClick`, `onChange`, `action`, `onSubmit`, …) must be plain inline or named functions. The compiler memoizes them. |
| 23 | +4. **Values derived from props / state** should be plain `const` declarations. If the derivation is expensive *and* measurably slow, consider extracting to a separate module or a hook — but still no `useMemo`. |
| 24 | +5. **Exception — `useEffectEvent`**: when you need a callback inside an effect that should read the latest props/state without triggering re-runs, use `useEffectEvent` (see `use-effect-event.md`). This is complementary to `'use memo'` and does not replace it. |
| 25 | +6. **Exception — identity-sensitive external APIs**: if a third-party API requires a stable reference across renders (e.g., a subscription that can only be installed once), prefer `useRef` or an effect-scoped closure over `useCallback`. `useCallback`'s identity guarantees are still weaker than the compiler's. |
| 26 | + |
| 27 | +## Pattern |
| 28 | + |
| 29 | +### ✅ Correct — plain functions, `'use memo'` directive |
| 30 | + |
| 31 | +```tsx |
| 32 | +const UserProfile: React.FC<Props> = ({ user, onSelect }) => { |
| 33 | + 'use memo'; |
| 34 | + const { t } = useTranslation(); |
| 35 | + |
| 36 | + const fullName = `${user.firstName} ${user.lastName}`; |
| 37 | + |
| 38 | + const handleClick = () => { |
| 39 | + onSelect(user.id); |
| 40 | + }; |
| 41 | + |
| 42 | + return ( |
| 43 | + <button onClick={handleClick}> |
| 44 | + {t('userProfile.Greet', { name: fullName })} |
| 45 | + </button> |
| 46 | + ); |
| 47 | +}; |
| 48 | +``` |
| 49 | + |
| 50 | +### ✅ Correct — custom hook with plain returned functions |
| 51 | + |
| 52 | +```tsx |
| 53 | +export const useSomething = (): [Value, (next: Value) => void] => { |
| 54 | + 'use memo'; |
| 55 | + const [value, setValue] = useState<Value>(initial); |
| 56 | + |
| 57 | + const update = (next: Value) => { |
| 58 | + setValue(next); |
| 59 | + // side effects here, reading latest closure values |
| 60 | + }; |
| 61 | + |
| 62 | + return [value, update]; |
| 63 | +}; |
| 64 | +``` |
| 65 | + |
| 66 | +### ❌ Wrong — manual `useCallback` wrapping a simple handler |
| 67 | + |
| 68 | +```tsx |
| 69 | +const UserProfile: React.FC<Props> = ({ user, onSelect }) => { |
| 70 | + // no 'use memo' |
| 71 | + const handleClick = useCallback(() => { |
| 72 | + onSelect(user.id); |
| 73 | + }, [onSelect, user.id]); |
| 74 | + |
| 75 | + return <button onClick={handleClick}>...</button>; |
| 76 | +}; |
| 77 | +``` |
| 78 | + |
| 79 | +### ❌ Wrong — `useMemo` for trivial derivations |
| 80 | + |
| 81 | +```tsx |
| 82 | +const UserProfile: React.FC<Props> = ({ user }) => { |
| 83 | + // no 'use memo' |
| 84 | + const fullName = useMemo( |
| 85 | + () => `${user.firstName} ${user.lastName}`, |
| 86 | + [user.firstName, user.lastName], |
| 87 | + ); |
| 88 | + return <span>{fullName}</span>; |
| 89 | +}; |
| 90 | +``` |
| 91 | + |
| 92 | +## Migration |
| 93 | + |
| 94 | +When editing a file that contains `useMemo` or `useCallback`: |
| 95 | + |
| 96 | +1. Add `'use memo'` to the enclosing component / hook body if it is not already there. |
| 97 | +2. Unwrap each `useMemo(() => expr, deps)` into `const value = expr;`. |
| 98 | +3. Unwrap each `useCallback(fn, deps)` into the original function body, as a plain `const fn = (...)` or inline in JSX. |
| 99 | +4. Drop the `useMemo` / `useCallback` import if it becomes unused. |
| 100 | +5. Run `bash scripts/verify.sh` — the React Compiler is part of the build pipeline; if the compiler cannot safely memoize something (e.g., mutation in a closure), the lint rule `react-hooks/*` or the build itself will surface the problem. |
| 101 | + |
| 102 | +## Verification |
| 103 | + |
| 104 | +After editing a memoization-relevant file, confirm: |
| 105 | + |
| 106 | +- `'use memo'` appears on the first line of the component / hook body. |
| 107 | +- No new `useMemo` or `useCallback` imports or calls were introduced. |
| 108 | +- `bash scripts/verify.sh` passes — the compiler is exercised during `pnpm run lint` and the TypeScript build. |
| 109 | +- For hooks: existing call sites still behave correctly. The compiler's output is observably equivalent to the hand-rolled version in almost every case; if not, the compiler surfaces a diagnostic. |
| 110 | + |
| 111 | +## Related |
| 112 | + |
| 113 | +- `use-effect-event.md` — companion rule for the `useEffectEvent` primitive used inside effects. |
| 114 | +- `CLAUDE.md` → "React Essentials" — project-wide React conventions, including the `'use memo'` requirement. |
0 commit comments