@wangs-ui/skills 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ ---
2
+ name: react19-compiler-typescript
3
+ description: Enforce idiomatic React 19 + TypeScript conventions built around the React Compiler's automatic memoization. Use this any time writing, generating, reviewing, or refactoring React components, hooks, or props in TypeScript/TSX — including code that manually wraps things in useMemo/useCallback/React.memo, uses forwardRef, mutates props/state, or needs typing for Actions, useOptimistic, use(), or refs. Trigger even if the user didn't say "React 19" or "compiler" explicitly; it applies whenever React component/hook code is being written or optimized.
4
+ ---
5
+
6
+ # React 19 + TypeScript with the React Compiler
7
+
8
+ ## Why this matters
9
+
10
+ React Compiler (stable since React Compiler 1.0, October 2025) rewrites your components
11
+ and hooks at build time, inserting memoization equivalent to `useMemo`/`useCallback`/
12
+ `React.memo` automatically and more granularly than a human would by hand. It ships as
13
+ `babel-plugin-react-compiler`, and its lint rules live inside `eslint-plugin-react-hooks`
14
+ (recommended preset) so linting and compilation share one source of truth.
15
+
16
+ The practical consequence: **manual memoization is no longer the default** — it's
17
+ either redundant, or actively harmful if it doesn't match what the compiler would have
18
+ inferred (the compiler bails out silently rather than risk breaking your app). Writing
19
+ "optimized" React in 2026 means writing _plain, rule-following_ React and trusting the
20
+ build step, not sprinkling `useMemo` everywhere out of habit.
21
+
22
+ This skill assumes and builds on the base `typescript-strict-typing` skill for general
23
+ typing discipline (no `any`, `interface` for entities, discriminated unions for variant
24
+ state, etc.) — apply both together.
25
+
26
+ ## Core principle
27
+
28
+ > Write plain, obviously-pure React. Let the compiler memoize. The Rules of React are no
29
+ > longer just style guidance — the compiler's correctness depends on you following them.
30
+
31
+ ---
32
+
33
+ ## 1. Stop hand-rolling memoization
34
+
35
+ > ⚠️ **Everything in this section assumes the compiler is confirmed active** (wired per
36
+ > §7, verified via the "Memo ✨" badge in §8). If you drop manual memoization _without_
37
+ > that confirmation, you don't get automatic memoization to replace it — you get
38
+ > **neither**. That's not a correctness bug (React still renders the right output), but
39
+ > every child re-renders on every parent render regardless of whether its props
40
+ > actually changed, and every inline computation reruns every render with nothing
41
+ > caching it. It's the pre-memoization default behavior of React — often invisible in
42
+ > small trees, but a real source of jank in large lists, heavy computations, or deep
43
+ > trees under a frequently-re-rendering parent. If you're not certain the compiler is
44
+ > active yet, keep existing manual memoization until you've verified it, then remove it.
45
+
46
+ Don't reach for `useMemo`, `useCallback`, or `React.memo` by default — the compiler adds
47
+ this automatically wherever it determines it helps.
48
+
49
+ ```tsx
50
+ // ❌ Old habit — noisy, and a mismatched dependency array is a whole class of bugs
51
+ const filteredUsers = useMemo(() => users.filter((u) => u.isActive), [users]);
52
+ const handleClick = useCallback(() => onSelect(user.id), [onSelect, user.id]);
53
+
54
+ // ✅ New default — just write the logic; the compiler memoizes what's worth memoizing
55
+ const filteredUsers = users.filter((u) => u.isActive);
56
+ const handleClick = () => onSelect(user.id);
57
+ ```
58
+
59
+ Manual memoization is still justified, narrowly, when:
60
+
61
+ - You've **confirmed a compiler bail-out** (see §6) on a genuine hot path via profiling,
62
+ and fixing the underlying Rules-of-React violation isn't possible right now.
63
+ - A value must have **stable referential identity across a boundary the compiler can't
64
+ see** — e.g. passed into a non-React library, a WebSocket subscription, or a
65
+ third-party hook incompatible with the compiler (`react-hook-form`'s `useForm`,
66
+ `@tanstack/react-table`'s `useReactTable` are known cases).
67
+ - Keep any manual memoization it produces isolated and commented with _why_, so it
68
+ doesn't silently rot into a bail-out later when the code around it changes.
69
+
70
+ ## 2. The Rules of React are now load-bearing
71
+
72
+ The compiler assumes your components and hooks are pure. Violating these rules doesn't
73
+ just risk a subtle bug anymore — it causes the compiler to silently skip optimizing that
74
+ component:
75
+
76
+ - **Idempotent renders** — given the same props/state/context, a component must return
77
+ the same output. No random values, no `Date.now()`, no side effects during render.
78
+ - **Immutability** — never mutate props, state, or context directly. Always create new
79
+ objects/arrays for changes.
80
+ - **Side effects only in effects or event handlers** — never during render.
81
+ - **Hooks called unconditionally, top-level, same order every render** — no hooks inside
82
+ conditionals, loops, or nested functions.
83
+
84
+ ```tsx
85
+ // ❌ Mutates a prop — breaks purity and the compiler can't safely memoize this
86
+ function TodoList({ todos }: { todos: Todo[] }) {
87
+ todos.sort((a, b) => a.priority - b.priority); // mutates caller's array
88
+ return (
89
+ <ul>
90
+ {todos.map((t) => (
91
+ <li key={t.id}>{t.title}</li>
92
+ ))}
93
+ </ul>
94
+ );
95
+ }
96
+
97
+ // ✅ Creates a new array — pure, compiler-safe
98
+ function TodoList({ todos }: { todos: Todo[] }) {
99
+ const sorted = [...todos].sort((a, b) => a.priority - b.priority);
100
+ return (
101
+ <ul>
102
+ {sorted.map((t) => (
103
+ <li key={t.id}>{t.title}</li>
104
+ ))}
105
+ </ul>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ## 3. Naming conventions the compiler relies on
111
+
112
+ The compiler identifies what to optimize by naming heuristics, same as the Rules of
113
+ Hooks linter:
114
+
115
+ | Kind | Convention | Notes |
116
+ | ------------------------------------------------------------------------ | ------------------------------- | ----------------------------------------------------------------------------- |
117
+ | Components | `PascalCase`, returns JSX | Compiler treats it as a component to optimize |
118
+ | Custom hooks | `camelCase`, prefixed `use` | Required for both Rules-of-Hooks lint and compiler analysis |
119
+ | Plain helper functions that return JSX-like values but aren't components | Avoid `PascalCase`/`use` naming | Prevents the compiler (and other devs) from mistaking it for a component/hook |
120
+
121
+ ## 4. Typing React 19 primitives
122
+
123
+ **`ref` as a normal prop** — `forwardRef` is no longer required for most cases; function
124
+ components can accept `ref` directly.
125
+
126
+ ```tsx
127
+ type InputProps = {
128
+ ref?: React.Ref<HTMLInputElement>;
129
+ placeholder?: string;
130
+ };
131
+
132
+ function TextInput({ ref, placeholder }: InputProps) {
133
+ return <input ref={ref} placeholder={placeholder} />;
134
+ }
135
+ ```
136
+
137
+ **Actions with `useActionState`** — type the state and payload as generics; model the
138
+ result as a discriminated union (per the base typing skill) rather than optional fields.
139
+
140
+ ```tsx
141
+ type FormState = { status: 'idle' } | { status: 'error'; message: string } | { status: 'success' };
142
+
143
+ const [state, formAction, isPending] = useActionState<FormState, FormData>(
144
+ async (_previous, formData) => {
145
+ const email = formData.get('email');
146
+ if (typeof email !== 'string' || !email.includes('@')) {
147
+ return { status: 'error', message: 'Invalid email' };
148
+ }
149
+ await submit(email);
150
+ return { status: 'success' };
151
+ },
152
+ { status: 'idle' },
153
+ );
154
+ ```
155
+
156
+ **Optimistic updates with `useOptimistic`** — type both the state and the update shape.
157
+
158
+ ```tsx
159
+ const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], Todo>(
160
+ todos,
161
+ (state, newTodo) => [...state, newTodo],
162
+ );
163
+ ```
164
+
165
+ **Reading a promise or context with `use()`** — type the resolved value, not the
166
+ promise wrapper; `use()` is not a hook and may be called conditionally.
167
+
168
+ ```tsx
169
+ function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
170
+ const comments = use(commentsPromise); // suspends until resolved
171
+ return (
172
+ <ul>
173
+ {comments.map((c) => (
174
+ <li key={c.id}>{c.text}</li>
175
+ ))}
176
+ </ul>
177
+ );
178
+ }
179
+ ```
180
+
181
+ **Stable event callbacks with `useEffectEvent`** (React 19.2+) — separates "event"
182
+ logic from "reactive" effect logic so the callback always sees the latest props/state
183
+ without being listed as an effect dependency. Needs `eslint-plugin-react-hooks@6+` to
184
+ lint correctly.
185
+
186
+ ```tsx
187
+ const onVisit = useEffectEvent((url: string) => {
188
+ logVisit(url, theme); // always fresh `theme`, never re-triggers the effect
189
+ });
190
+
191
+ useEffect(() => {
192
+ onVisit(url);
193
+ }, [url]); // `theme` intentionally omitted — onVisit is stable
194
+ ```
195
+
196
+ ## 5. Compiler-friendly render patterns
197
+
198
+ - Creating new object/array/function literals inline in render (`style={{ color }}`,
199
+ `onClick={() => ...}`) is fine — stop manually hoisting or `useMemo`-wrapping these
200
+ preemptively; the compiler memoizes them if it determines it's worthwhile.
201
+ - Avoid module-level mutable variables read or written during render — that state is
202
+ invisible to the compiler and breaks idempotence.
203
+ - Don't use `useRef` to store a value that should trigger a re-render when it changes —
204
+ refs are an imperative escape hatch, not state, and the compiler treats them as such.
205
+ - Keep components small and composable. The compiler optimizes per component/hook
206
+ boundary, so a single 300-line component gives it far less to work with than several
207
+ focused ones.
208
+
209
+ ## 6. Typing props (builds on `typescript-strict-typing`)
210
+
211
+ - `interface` for a component's `Props` — it's an entity shape, often extended.
212
+ - A discriminated union when a component has mutually exclusive prop combinations,
213
+ instead of a pile of optional props that can contradict each other.
214
+
215
+ ```tsx
216
+ // ❌ Bad — nothing stops passing both `href` and `onClick` incoherently
217
+ interface ButtonProps {
218
+ label: string;
219
+ href?: string;
220
+ onClick?: () => void;
221
+ }
222
+
223
+ // ✅ Good — the two variants can't be mixed
224
+ type ButtonProps =
225
+ | { variant: 'link'; label: string; href: string }
226
+ | { variant: 'action'; label: string; onClick: () => void };
227
+ ```
228
+
229
+ ## 7. Tooling setup
230
+
231
+ **The compiler is opt-in — no default setup enables it automatically.** Plain
232
+ `@vitejs/plugin-react` (`react()`), plain Next.js, plain Babel/webpack config, etc. do
233
+ **not** run the compiler on their own. Verify it's actually wired up before assuming any
234
+ of the memoization guidance above applies to your build.
235
+
236
+ ```bash
237
+ # Compiler (build-time transform)
238
+ npm install --save-dev --save-exact babel-plugin-react-compiler@latest
239
+ ```
240
+
241
+ **Lint rules — oxlint.** Oxlint ships a **native, Rust-based** `react/react-compiler`
242
+ rule that runs the same compiler analysis in lint-only mode — same diagnostics as the
243
+ Babel-based ESLint version, no Babel needed for linting. It's experimental and **off by
244
+ default**, so it has to be enabled explicitly:
245
+
246
+ ```json
247
+ // .oxlintrc.json
248
+ {
249
+ "plugins": ["react"],
250
+ "rules": {
251
+ "react/react-compiler": "error"
252
+ }
253
+ }
254
+ ```
255
+
256
+ This single rule reports two distinct things — both worth fixing, but for different
257
+ reasons:
258
+
259
+ - **Rules-of-React violations** (conditional hooks, reading a ref during render, mutating
260
+ props) — these are real bugs, independent of the compiler.
261
+ - **Compiler bail-outs** — places the compiler declined to optimize (e.g. unsupported
262
+ syntax) without a rule violation. Not incorrect code, just a missed optimization —
263
+ lower priority than a violation, but worth knowing about on a hot path.
264
+
265
+ If you'd rather use an existing ESLint plugin's rules through oxlint instead of the
266
+ native one (e.g. to match a team convention), oxlint's `jsPlugins` can load
267
+ `eslint-plugin-react-hooks` directly — slower than the native rule since it still runs
268
+ through Babel, but useful if you need a rule the native port doesn't cover yet:
269
+
270
+ ```json
271
+ {
272
+ "jsPlugins": [{ "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" }],
273
+ "rules": { "react-hooks-js/set-state-in-render": "error" }
274
+ }
275
+ ```
276
+
277
+ **Lint rules — ESLint** (if not on oxlint): the same rules ship inside
278
+ `eslint-plugin-react-hooks`.
279
+
280
+ ```bash
281
+ npm install --save-dev eslint-plugin-react-hooks@latest
282
+ ```
283
+
284
+ ```js
285
+ // eslint.config.js
286
+ import reactHooks from 'eslint-plugin-react-hooks';
287
+ import { defineConfig } from 'eslint/config';
288
+
289
+ export default defineConfig([reactHooks.configs.flat.recommended]);
290
+ ```
291
+
292
+ **Wiring it into Vite 8.** `@vitejs/plugin-react` v6+ (the version that ships with Vite 8) switched its default transform from Babel to oxc for speed, so the compiler is
293
+ **never** on by default and the old `react({ babel: {...} })` option **does not work**
294
+ on this setup — it's silently ignored, not an error, which is an easy way to think the
295
+ compiler is running when it isn't. Wire it in explicitly, as a separate Babel pass that
296
+ runs before `react()`:
297
+
298
+ ```js
299
+ // vite.config.js
300
+ import { defineConfig } from 'vite';
301
+ import react, { reactCompilerPreset } from '@vitejs/plugin-react';
302
+ import babel from '@rolldown/plugin-babel';
303
+
304
+ export default defineConfig({
305
+ plugins: [
306
+ babel({ presets: [reactCompilerPreset()] }), // must run before react()
307
+ react(),
308
+ ],
309
+ });
310
+ ```
311
+
312
+ ```bash
313
+ npm install --save-dev @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
314
+ npm install --save-dev @types/babel__core # if using TypeScript
315
+ ```
316
+
317
+ `reactCompilerPreset()` is a helper exported from `@vitejs/plugin-react` itself — it
318
+ bundles `babel-plugin-react-compiler` with sane default include/exclude filters so you
319
+ don't have to hand-roll a Babel preset. It optionally accepts:
320
+
321
+ - `compilationMode: 'annotation'` — only compile components explicitly marked with a
322
+ `"use memo"` directive, instead of the whole codebase (useful for a gradual rollout).
323
+ - `target: '17' | '18'` — if any part of the app still runs on an older React major and
324
+ needs the `react-compiler-runtime` package instead of `react/compiler-runtime`.
325
+
326
+ After adding this, confirm it's actually active via the React DevTools "Memo ✨" badge
327
+ (§8) before trusting the "don't hand-roll memoization" guidance in §1 — a silently
328
+ misconfigured Babel order (`react()` before `babel()`) is a common way for this to look
329
+ wired up but do nothing.
330
+
331
+ - Treat compiler-related lint errors (Rules-of-React violations, mismatched manual
332
+ memoization) as must-fix, not optional — an unfixed violation means that component
333
+ silently gets **zero** compiler optimization.
334
+ - For a large existing codebase, adopt incrementally by scoping the babel plugin to a
335
+ directory (e.g. a UI component library) before enabling it globally.
336
+ - If a specific function is genuinely incompatible with the compiler (e.g. it calls
337
+ `useForm` from `react-hook-form`), opt it out with the `"use no memo"` directive as
338
+ the **first line of the function body** — it's a temporary escape hatch, not a
339
+ permanent fix, so leave a comment explaining why.
340
+
341
+ ```tsx
342
+ function LegacyForm() {
343
+ 'use no memo';
344
+ const form = useForm(); // incompatible with the compiler today
345
+ // ...
346
+ }
347
+ ```
348
+
349
+ ## 8. Checking whether the compiler is actually optimizing
350
+
351
+ - **React DevTools** — an optimized component shows a "Memo ✨" badge next to its name
352
+ in the component tree.
353
+ - **ESLint** — the compiler's recommended rules flag Rules-of-React violations at lint
354
+ time, before they ever become a silent runtime bail-out.
355
+ - A bail-out is not a crash — it just means that specific component/hook is running
356
+ unoptimized. Treat a missing "Memo ✨" badge on a component you expect to be optimized
357
+ as a signal to check for a Rules-of-React violation, not a compiler bug.
358
+
359
+ ---
360
+
361
+ ## Review checklist
362
+
363
+ - [ ] Compiler confirmed active ("Memo ✨" badge) before removing any _existing_ manual
364
+ memoization — don't strip it on faith
365
+ - [ ] No new `useMemo`/`useCallback`/`React.memo` added without a documented reason
366
+ (confirmed bail-out, or a boundary the compiler can't see through)
367
+ - [ ] No prop/state/context mutation anywhere in render
368
+ - [ ] All hooks called unconditionally at the top level, same order every render
369
+ - [ ] Side effects live in `useEffect`/event handlers, never during render
370
+ - [ ] Components are `PascalCase`; hooks are `camelCase` and prefixed `use`
371
+ - [ ] `ref` accepted as a normal prop instead of `forwardRef`, unless targeting a version
372
+ that requires it
373
+ - [ ] Action/optimistic-update state modeled as a discriminated union, not optional
374
+ fields
375
+ - [ ] Mutually exclusive prop combinations modeled as a discriminated union `Props` type
376
+ - [ ] `eslint-plugin-react-hooks` recommended config enabled and passing
377
+ - [ ] Any `"use no memo"` usage has a comment explaining why
378
+
379
+ ## Quick reference
380
+
381
+ | Situation | Do |
382
+ | -------------------------------------------------------------- | ------------------------------------------------------------- |
383
+ | Tempted to write `useMemo`/`useCallback` | Don't — write the plain expression, let the compiler decide |
384
+ | Need a ref on a function component | Accept `ref` as a prop, skip `forwardRef` |
385
+ | Form/async state with distinct outcomes | Discriminated union via `useActionState`, not optional fields |
386
+ | Callback needs latest props/state without re-running an effect | `useEffectEvent` |
387
+ | A hook/library is known-incompatible with the compiler | `"use no memo"` at the top of that function, with a comment |
388
+ | Checking if optimization is happening | React DevTools "Memo ✨" badge + compiler ESLint rules |