@baselane/packs 0.1.4 → 0.1.5

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.
Files changed (97) hide show
  1. package/dist/aliases.js +1 -0
  2. package/package.json +2 -2
  3. package/packs/database-review/pack.json +75 -9
  4. package/packs/database-review/rendered/.claude/agents/database-reviewer.md +75 -2
  5. package/packs/database-review/rendered/.claude/commands/migration-review.md +6 -0
  6. package/packs/database-review/rendered/.claude/settings.json +11 -0
  7. package/packs/database-review/rendered/.claude/skills/database-migrations/SKILL.md +431 -0
  8. package/packs/database-review/rendered/.claude/skills/mysql-patterns/SKILL.md +414 -0
  9. package/packs/database-review/rendered/.claude/skills/postgres-patterns/SKILL.md +149 -0
  10. package/packs/database-review/rendered/.github/copilot-instructions.md +1096 -14
  11. package/packs/database-review/rendered/AGENTS.md +45 -16
  12. package/packs/database-review/rendered/CLAUDE.md +4 -0
  13. package/packs/database-review/rendered/GEMINI.md +987 -0
  14. package/packs/go-rules/pack.json +65 -9
  15. package/packs/go-rules/rendered/.claude/agents/go-build-resolver.md +90 -0
  16. package/packs/go-rules/rendered/.claude/agents/go-reviewer.md +72 -2
  17. package/packs/go-rules/rendered/.claude/commands/go-build.md +6 -0
  18. package/packs/go-rules/rendered/.claude/commands/go-review.md +1 -1
  19. package/packs/go-rules/rendered/.claude/commands/go-test.md +6 -0
  20. package/packs/go-rules/rendered/.claude/settings.json +9 -0
  21. package/packs/go-rules/rendered/.claude/skills/golang-patterns/SKILL.md +673 -0
  22. package/packs/go-rules/rendered/.claude/skills/golang-testing/SKILL.md +719 -0
  23. package/packs/go-rules/rendered/.github/copilot-instructions.md +1783 -7
  24. package/packs/go-rules/rendered/AGENTS.md +244 -8
  25. package/packs/go-rules/rendered/CLAUDE.md +5 -0
  26. package/packs/go-rules/rendered/GEMINI.md +1394 -0
  27. package/packs/python-rules/pack.json +87 -10
  28. package/packs/python-rules/rendered/.claude/agents/django-reviewer.md +159 -0
  29. package/packs/python-rules/rendered/.claude/agents/fastapi-reviewer.md +70 -0
  30. package/packs/python-rules/rendered/.claude/agents/python-reviewer.md +92 -2
  31. package/packs/python-rules/rendered/.claude/commands/django-review.md +6 -0
  32. package/packs/python-rules/rendered/.claude/commands/fastapi-review.md +6 -0
  33. package/packs/python-rules/rendered/.claude/commands/python-review.md +1 -1
  34. package/packs/python-rules/rendered/.claude/settings.json +1 -1
  35. package/packs/python-rules/rendered/.claude/skills/django-security/SKILL.md +642 -0
  36. package/packs/python-rules/rendered/.claude/skills/django-tdd/SKILL.md +728 -0
  37. package/packs/python-rules/rendered/.claude/skills/fastapi-patterns/SKILL.md +512 -0
  38. package/packs/python-rules/rendered/.claude/skills/python-patterns/SKILL.md +749 -0
  39. package/packs/python-rules/rendered/.claude/skills/python-testing/SKILL.md +815 -0
  40. package/packs/python-rules/rendered/.github/copilot-instructions.md +3815 -12
  41. package/packs/python-rules/rendered/AGENTS.md +73 -13
  42. package/packs/python-rules/rendered/CLAUDE.md +9 -0
  43. package/packs/python-rules/rendered/GEMINI.md +3448 -0
  44. package/packs/security-review/pack.json +51 -7
  45. package/packs/security-review/rendered/.claude/agents/security-reviewer.md +29 -2
  46. package/packs/security-review/rendered/.claude/settings.json +11 -0
  47. package/packs/security-review/rendered/.claude/skills/security-review/SKILL.md +302 -0
  48. package/packs/security-review/rendered/.claude/skills/security-review/cloud-infrastructure-security.md +171 -0
  49. package/packs/security-review/rendered/.claude/skills/security-scan-config/SKILL.md +56 -0
  50. package/packs/security-review/rendered/.github/copilot-instructions.md +658 -15
  51. package/packs/security-review/rendered/AGENTS.md +283 -17
  52. package/packs/security-review/rendered/CLAUDE.md +2 -0
  53. package/packs/security-review/rendered/GEMINI.md +352 -0
  54. package/packs/software-engineer-harness/pack.json +249 -13
  55. package/packs/software-engineer-harness/rendered/.claude/agents/code-explorer.md +67 -0
  56. package/packs/software-engineer-harness/rendered/.claude/agents/performance-optimizer.md +444 -0
  57. package/packs/software-engineer-harness/rendered/.claude/agents/pr-test-analyzer.md +43 -0
  58. package/packs/software-engineer-harness/rendered/.claude/commands/feature-dev.md +6 -0
  59. package/packs/software-engineer-harness/rendered/.claude/settings.json +29 -0
  60. package/packs/software-engineer-harness/rendered/.claude/skills/api-design/SKILL.md +522 -0
  61. package/packs/software-engineer-harness/rendered/.claude/skills/architecture-decision-records/SKILL.md +178 -0
  62. package/packs/software-engineer-harness/rendered/.claude/skills/backend-patterns/SKILL.md +560 -0
  63. package/packs/software-engineer-harness/rendered/.claude/skills/codebase-onboarding/SKILL.md +232 -0
  64. package/packs/software-engineer-harness/rendered/.claude/skills/deployment-patterns/SKILL.md +426 -0
  65. package/packs/software-engineer-harness/rendered/.claude/skills/docker-patterns/SKILL.md +363 -0
  66. package/packs/software-engineer-harness/rendered/.claude/skills/e2e-testing/SKILL.md +325 -0
  67. package/packs/software-engineer-harness/rendered/.claude/skills/tdd-workflow/SKILL.md +580 -0
  68. package/packs/software-engineer-harness/rendered/.github/copilot-instructions.md +3948 -2
  69. package/packs/software-engineer-harness/rendered/AGENTS.md +194 -4
  70. package/packs/software-engineer-harness/rendered/CLAUDE.md +20 -0
  71. package/packs/software-engineer-harness/rendered/GEMINI.md +3208 -0
  72. package/packs/typescript-rules/pack.json +68 -5
  73. package/packs/typescript-rules/rendered/.claude/agents/react-reviewer.md +165 -0
  74. package/packs/typescript-rules/rendered/.claude/agents/type-design-analyzer.md +48 -0
  75. package/packs/typescript-rules/rendered/.claude/commands/react-review.md +6 -0
  76. package/packs/typescript-rules/rendered/.claude/settings.json +9 -0
  77. package/packs/typescript-rules/rendered/.claude/skills/react-patterns/SKILL.md +340 -0
  78. package/packs/typescript-rules/rendered/.claude/skills/react-performance/SKILL.md +573 -0
  79. package/packs/typescript-rules/rendered/.claude/skills/react-testing/SKILL.md +422 -0
  80. package/packs/typescript-rules/rendered/.github/copilot-instructions.md +2459 -53
  81. package/packs/typescript-rules/rendered/AGENTS.md +893 -17
  82. package/packs/typescript-rules/rendered/CLAUDE.md +6 -0
  83. package/packs/typescript-rules/rendered/GEMINI.md +1335 -0
  84. package/packs/ecc-essentials/pack.json +0 -93
  85. package/packs/ecc-essentials/rendered/.claude/settings.json +0 -37
  86. package/packs/ecc-essentials/rendered/.github/copilot-instructions.md +0 -110
  87. package/packs/ecc-essentials/rendered/AGENTS.md +0 -74
  88. package/packs/ecc-essentials/rendered/CLAUDE.md +0 -14
  89. package/packs/ecc-essentials/rendered/GEMINI.md +0 -25
  90. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/agents/build-error-resolver.md +0 -0
  91. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/agents/refactor-cleaner.md +0 -0
  92. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/agents/silent-failure-hunter.md +0 -0
  93. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/agents/tdd-guide.md +0 -0
  94. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/commands/fix-build.md +0 -0
  95. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/commands/hunt-silent-failures.md +0 -0
  96. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/commands/tdd.md +0 -0
  97. /package/packs/{ecc-essentials → software-engineer-harness}/rendered/.claude/skills/tdd-red-green-refactor/SKILL.md +0 -0
@@ -49,3 +49,1338 @@ function rename(user, name) {
49
49
  A locally-scoped object that never escapes the function — a builder/accumulator you allocated and
50
50
  return once — is fine to mutate. The rule protects values that are *shared*: arguments, fields on
51
51
  long-lived state, anything another holder still references.
52
+
53
+ ### react-patterns
54
+
55
+ Use when writing or reviewing React function components, custom hooks, or component trees — covers hooks discipline, state location, Server/Client Component boundaries, Suspense + error boundaries, form actions, data fetching, and accessibility-first composition for React 18/19.
56
+
57
+ # React Patterns
58
+
59
+ Idiomatic React 18/19 patterns for building robust, accessible, performant component trees.
60
+
61
+ ## When to Activate
62
+
63
+ - Writing or modifying React function components, custom hooks, or component trees
64
+ - Reviewing JSX/TSX files
65
+ - Designing state shape or component composition
66
+ - Migrating class components or older `forwardRef`/`useEffect`-heavy code
67
+ - Choosing between local state, lifted state, context, and external stores
68
+ - Working with Server Components / Client Components (Next.js App Router, RSC)
69
+ - Implementing forms with React 19 actions or controlled inputs
70
+ - Wiring data fetching with TanStack Query / SWR / RSC
71
+
72
+ ## Core Principles
73
+
74
+ ### 1. Render is a Pure Function of Props and State
75
+
76
+ ```tsx
77
+ // Good: derive during render
78
+ function Cart({ items }: { items: CartItem[] }) {
79
+ const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
80
+ return <span>{formatMoney(total)}</span>;
81
+ }
82
+
83
+ // Bad: derived state stored separately
84
+ function Cart({ items }: { items: CartItem[] }) {
85
+ const [total, setTotal] = useState(0);
86
+ useEffect(() => {
87
+ setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
88
+ }, [items]);
89
+ return <span>{formatMoney(total)}</span>;
90
+ }
91
+ ```
92
+
93
+ Derived state in `useEffect` adds a render cycle, can desync, and obscures the data flow.
94
+
95
+ ### 2. Side Effects Outside Render
96
+
97
+ Effects, mutations, network calls, and subscriptions live in event handlers or `useEffect` — never in the render body.
98
+
99
+ ### 3. Composition Over Inheritance
100
+
101
+ React has no inheritance model for components. Compose with `children`, render props, or component props.
102
+
103
+ ## Hooks Discipline
104
+
105
+ See [rules/react/hooks.md](../../rules/react/hooks.md) for the full ruleset. Highlights:
106
+
107
+ - Top-level only, never conditional
108
+ - Cleanup every subscription, interval, listener
109
+ - Functional updater (`setX(prev => prev + 1)`) when new state depends on old
110
+ - Default position: do not memoize — add `useMemo`/`useCallback` only when a profiler or a dependency chain proves it matters
111
+ - Extract a custom hook only when the same hook sequence appears in 2+ components
112
+
113
+ ## State Location Decision Tree
114
+
115
+ ```
116
+ Used by one component?
117
+ -> useState inside it
118
+
119
+ Used by parent + a few descendants?
120
+ -> lift to nearest common ancestor
121
+
122
+ Used across distant branches AND low-frequency reads (theme, auth, locale)?
123
+ -> React Context
124
+
125
+ High-frequency updates shared across the tree?
126
+ -> external store (Zustand, Jotai, Redux Toolkit)
127
+
128
+ Derived from a server?
129
+ -> server-state library (TanStack Query, SWR, RSC fetch)
130
+ ```
131
+
132
+ Most pages do not need context or a global store. Resist abstraction until duplicated lifting becomes painful.
133
+
134
+ ## Server / Client Components (RSC)
135
+
136
+ ```tsx
137
+ // Server Component - default, async, never ships JS for itself
138
+ export default async function ProductPage({ params }: { params: { id: string } }) {
139
+ const product = await db.product.findUnique({ where: { id: params.id } });
140
+ if (!product) notFound();
141
+ return <ProductView product={product} />;
142
+ }
143
+
144
+ // Client Component - opt in with "use client"
145
+ "use client";
146
+ export function AddToCartButton({ productId }: { productId: string }) {
147
+ const [pending, startTransition] = useTransition();
148
+ return (
149
+ <button
150
+ disabled={pending}
151
+ onClick={() => startTransition(() => addToCart(productId))}
152
+ >
153
+ {pending ? "Adding..." : "Add to cart"}
154
+ </button>
155
+ );
156
+ }
157
+ ```
158
+
159
+ Boundaries:
160
+
161
+ - Server -> Client: pass serializable props or `children`
162
+ - Client -> Server: invoke Server Actions via `<form action={...}>` or imperatively from event handlers
163
+ - Never `import` a Server Component from a Client Component file — compose them via `children` instead
164
+
165
+ ## Suspense + Error Boundaries
166
+
167
+ ```tsx
168
+ <ErrorBoundary fallback={<ErrorView />}>
169
+ <Suspense fallback={<UserSkeleton />}>
170
+ <UserDetail id={id} />
171
+ </Suspense>
172
+ </ErrorBoundary>
173
+ ```
174
+
175
+ - Place Suspense boundaries close to the data, not at the route root — progressively reveal content
176
+ - Error Boundary remains a class API; use `react-error-boundary` for a hook-friendly wrapper
177
+ - A boundary catches errors thrown during render, lifecycle, and constructors of its children — NOT in event handlers or async code
178
+
179
+ ## Forms
180
+
181
+ ### React 19 form actions (preferred for new code)
182
+
183
+ ```tsx
184
+ "use client";
185
+ import { useActionState } from "react";
186
+
187
+ const initial = { error: null as string | null };
188
+
189
+ async function updateUserAction(_prev: typeof initial, formData: FormData) {
190
+ "use server";
191
+ const parsed = UserSchema.safeParse(Object.fromEntries(formData));
192
+ if (!parsed.success) return { error: "Invalid input" };
193
+ await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
194
+ return { error: null };
195
+ }
196
+
197
+ export function UserForm() {
198
+ const [state, formAction, pending] = useActionState(updateUserAction, initial);
199
+ return (
200
+ <form action={formAction}>
201
+ <input name="name" required />
202
+ <button type="submit" disabled={pending}>Save</button>
203
+ {state.error && <p role="alert">{state.error}</p>}
204
+ </form>
205
+ );
206
+ }
207
+ ```
208
+
209
+ ### Controlled inputs
210
+
211
+ Use controlled when the value drives other UI, formats on every keystroke, or implements real-time validation.
212
+
213
+ ### Complex forms
214
+
215
+ For multi-step forms, dynamic field arrays, or cross-field validation: use a library (React Hook Form, TanStack Form). Roll-your-own state management for forms past trivial complexity is a maintenance trap.
216
+
217
+ ## Data Fetching Decision Matrix
218
+
219
+ | Need | Tool |
220
+ |---|---|
221
+ | Per-request data in Next.js App Router | RSC `await fetch()` |
222
+ | Client-side cache + mutations + invalidation | TanStack Query |
223
+ | Lightweight client cache + revalidation | SWR |
224
+ | Real-time subscriptions | Server-Sent Events, WebSockets, or the lib's subscription API |
225
+ | One-off fire-and-forget | `fetch()` in an event handler |
226
+
227
+ Avoid `useEffect` + `fetch` for application data — race conditions, no cache, no retry, no Suspense integration.
228
+
229
+ ## Composition Recipes
230
+
231
+ ### Slot via `children`
232
+
233
+ ```tsx
234
+ <Layout>
235
+ <Header />
236
+ <Main>{content}</Main>
237
+ </Layout>
238
+ ```
239
+
240
+ ### Named slots
241
+
242
+ ```tsx
243
+ <Page header={<Nav />} sidebar={<Filters />}>
244
+ <Results />
245
+ </Page>
246
+ ```
247
+
248
+ ### Compound components (shared state via Context)
249
+
250
+ ```tsx
251
+ <Tabs defaultValue="profile">
252
+ <Tabs.List>
253
+ <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
254
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
255
+ </Tabs.List>
256
+ <Tabs.Panel value="profile"><Profile /></Tabs.Panel>
257
+ <Tabs.Panel value="settings"><Settings /></Tabs.Panel>
258
+ </Tabs>
259
+ ```
260
+
261
+ ### Render prop / function-as-child
262
+
263
+ Useful when the parent needs to pass parameters to the rendered output:
264
+
265
+ ```tsx
266
+ <DataLoader id={id}>
267
+ {({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
268
+ </DataLoader>
269
+ ```
270
+
271
+ Modern alternative: a hook (`useData(id)`) returning the same shape — usually cleaner.
272
+
273
+ ## Performance
274
+
275
+ ### When `React.memo` Actually Helps
276
+
277
+ Wrap a component in `React.memo` only when:
278
+
279
+ 1. It re-renders frequently
280
+ 2. Its props are usually the same between renders
281
+ 3. Its render is measurably expensive
282
+
283
+ `React.memo` adds an equality check on every render. If props differ on most renders, the check is pure overhead.
284
+
285
+ ### Avoiding Render Cascades
286
+
287
+ - Lift state down rather than up where possible
288
+ - Split context: one context per concern, so a change to `themeContext` does not re-render auth consumers
289
+ - Use `useSyncExternalStore` for external state libraries — required for safe concurrent rendering
290
+
291
+ ### Lists
292
+
293
+ - Provide stable `key` props (database id, not array index)
294
+ - Virtualize long lists with `@tanstack/react-virtual` or `react-window` once visible item count exceeds ~50 with non-trivial rows
295
+
296
+ ## Accessibility-First Composition
297
+
298
+ - Always render semantic HTML (`<button>`, `<a>`, `<nav>`, `<main>`) before reaching for `role` attributes
299
+ - Every interactive element must be reachable by keyboard
300
+ - Form inputs need labels — `<label htmlFor>` or `aria-label` if visually labeled by an icon
301
+ - Manage focus on route changes and modal open/close
302
+ - Run `axe` in component tests (see [skills/react-testing](../react-testing/SKILL.md))
303
+ - Cross-link: [skills/accessibility/SKILL.md](../accessibility/SKILL.md) covers WCAG criteria and pattern libraries
304
+
305
+ ## Routing
306
+
307
+ This skill is router-agnostic. The patterns above work with React Router, TanStack Router, Next.js App Router, Remix Router. Router-specific patterns (loaders, actions, nested layouts) follow the router's documentation — those are framework concerns layered on top of React core.
308
+
309
+ ## Out of Scope (Pointer Sections)
310
+
311
+ - **Next.js specifics**: App Router data loading, Route Handlers, Middleware, Parallel Routes — separate concern, use Next.js docs
312
+ - **React Native**: Platform-specific patterns differ enough to warrant a separate `react-native-patterns` skill (not present yet)
313
+ - **Remix**: Loader/action conventions overlap with RSC but follow Remix docs
314
+
315
+ ## Related
316
+
317
+ - Rules: [rules/react/](../../rules/react/) — coding-style, hooks, patterns, security, testing
318
+ - Skills: [react-performance](../react-performance/SKILL.md) for the Vercel-derived performance ruleset, [frontend-patterns](../frontend-patterns/SKILL.md) for cross-framework UI concerns, [accessibility](../accessibility/SKILL.md), [angular-developer](../angular-developer/SKILL.md) for framework comparison
319
+ - Agents: `react-reviewer` for code review, `react-build-resolver` for build/bundler errors
320
+ - Commands: `/react-review`, `/react-build`, `/react-test`
321
+
322
+ ## Examples
323
+
324
+ ### Custom hook for debounced search
325
+
326
+ ```tsx
327
+ function useDebounce<T>(value: T, delay = 300): T {
328
+ const [debounced, setDebounced] = useState(value);
329
+ useEffect(() => {
330
+ const id = setTimeout(() => setDebounced(value), delay);
331
+ return () => clearTimeout(id);
332
+ }, [value, delay]);
333
+ return debounced;
334
+ }
335
+
336
+ function SearchBox() {
337
+ const [query, setQuery] = useState("");
338
+ const debounced = useDebounce(query, 300);
339
+ const { data } = useQuery({
340
+ queryKey: ["search", debounced],
341
+ queryFn: () => searchApi(debounced),
342
+ enabled: debounced.length > 0,
343
+ });
344
+ return (
345
+ <>
346
+ <input value={query} onChange={(e) => setQuery(e.target.value)} />
347
+ <Results items={data ?? []} />
348
+ </>
349
+ );
350
+ }
351
+ ```
352
+
353
+ ### Optimistic UI with React 19 `useOptimistic`
354
+
355
+ ```tsx
356
+ "use client";
357
+ import { useOptimistic } from "react";
358
+
359
+ export function MessageList({ messages }: { messages: Message[] }) {
360
+ const [optimistic, addOptimistic] = useOptimistic(
361
+ messages,
362
+ (state, newMessage: Message) => [...state, newMessage],
363
+ );
364
+
365
+ async function send(formData: FormData) {
366
+ const text = String(formData.get("text"));
367
+ addOptimistic({ id: "pending", text, sender: "me" });
368
+ await saveMessage(text);
369
+ }
370
+
371
+ return (
372
+ <>
373
+ <ul>{optimistic.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
374
+ <form action={send}>
375
+ <input name="text" />
376
+ <button type="submit">Send</button>
377
+ </form>
378
+ </>
379
+ );
380
+ }
381
+ ```
382
+
383
+ ### Splitting context to avoid render cascades
384
+
385
+ ```tsx
386
+ // Two contexts: one rarely changes, one frequently
387
+ const ThemeContext = createContext<Theme>("light");
388
+ const NotificationsContext = createContext<Notification[]>([]);
389
+
390
+ // A component that only consumes ThemeContext does NOT re-render when notifications change
391
+ ```
392
+
393
+ ### react-performance
394
+
395
+ Use when writing, reviewing, or refactoring React/Next.js code for performance — 70+ rules across waterfalls, bundle size, server-side performance, client fetching, re-renders, rendering, JS micro-perf, and advanced patterns, adapted from Vercel Engineering's React Best Practices.
396
+
397
+ # React Performance
398
+
399
+ Performance optimization patterns for React 18/19 and Next.js, adapted from [Vercel Labs `react-best-practices`](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices) (MIT, v1.0.0). This skill organizes rules by priority and provides decision-tree guidance for active code review and refactoring.
400
+
401
+ ## When to Activate
402
+
403
+ - Writing or reviewing React/Next.js code for performance
404
+ - Diagnosing slow page loads, slow interactions, or high CPU on the client
405
+ - Auditing bundle size or Lighthouse Core Web Vitals regressions
406
+ - Removing waterfalls in Server Components / API routes
407
+ - Reducing client-side re-renders
408
+ - Optimizing long lists, animations, or hydration
409
+ - Auditing optimization choices in PRs touching `app/`, `pages/`, `components/`, or data layers
410
+
411
+ ## Priority Index
412
+
413
+ | Priority | Category | Prefix | When it matters |
414
+ |---|---|---|---|
415
+ | 1 — CRITICAL | Eliminating Waterfalls | `async-` | Anytime `await` is followed by independent `await` |
416
+ | 2 — CRITICAL | Bundle Size Optimization | `bundle-` | First-load JS, route-level imports, third-party libs |
417
+ | 3 — HIGH | Server-Side Performance | `server-` | RSC, Server Actions, API routes, SSR |
418
+ | 4 — MEDIUM-HIGH | Client-Side Data Fetching | `client-` | SWR / TanStack Query / raw `fetch` in hooks |
419
+ | 5 — MEDIUM | Re-render Optimization | `rerender-` | High-frequency state updates, parent-child fan-out |
420
+ | 6 — MEDIUM | Rendering Performance | `rendering-` | Long lists, animations, hydration |
421
+ | 7 — LOW-MEDIUM | JavaScript Performance | `js-` | Hot loops, frequent allocations |
422
+ | 8 — LOW | Advanced Patterns | `advanced-` | Effect-event integration, stable refs |
423
+
424
+ ## 1. Eliminating Waterfalls (CRITICAL)
425
+
426
+ > "Waterfalls are the #1 performance killer" — every sequential `await` adds full network latency.
427
+
428
+ ### Cheap conditions before await
429
+
430
+ Check sync conditions (props, env, hardcoded flags) before awaiting remote data.
431
+
432
+ ```ts
433
+ // INCORRECT
434
+ async function Page({ id }: { id: string }) {
435
+ const flag = await getFlag("show-page");
436
+ if (!flag || !id) return null;
437
+ const data = await getData(id);
438
+ // ...
439
+ }
440
+
441
+ // CORRECT — short-circuit on cheap sync condition first
442
+ async function Page({ id }: { id: string }) {
443
+ if (!id) return null;
444
+ const flag = await getFlag("show-page");
445
+ if (!flag) return null;
446
+ const data = await getData(id);
447
+ }
448
+ ```
449
+
450
+ ### Defer awaits until used
451
+
452
+ Move `await` into the branch that uses it.
453
+
454
+ ```ts
455
+ // INCORRECT — awaits before deciding it needs the data
456
+ const user = await getUser(id);
457
+ if (mode === "guest") return renderGuest();
458
+ return renderUser(user);
459
+
460
+ // CORRECT
461
+ if (mode === "guest") return renderGuest();
462
+ const user = await getUser(id);
463
+ return renderUser(user);
464
+ ```
465
+
466
+ ### Promise.all for independent work
467
+
468
+ ```ts
469
+ // INCORRECT — sequential
470
+ const user = await getUser(id);
471
+ const posts = await getPosts(id);
472
+ const followers = await getFollowers(id);
473
+
474
+ // CORRECT — parallel
475
+ const [user, posts, followers] = await Promise.all([
476
+ getUser(id),
477
+ getPosts(id),
478
+ getFollowers(id),
479
+ ]);
480
+ ```
481
+
482
+ ### Partial dependencies — start early, await late
483
+
484
+ ```ts
485
+ // CORRECT — kick off all promises, await only when each result is needed
486
+ const userP = getUser(id);
487
+ const postsP = getPosts(id);
488
+ const profile = await getProfile(id);
489
+ if (profile.private) return null;
490
+ const [user, posts] = await Promise.all([userP, postsP]);
491
+ ```
492
+
493
+ ### Suspense for streaming
494
+
495
+ Push `<Suspense>` boundaries close to the data so the page paints what it can while slower sub-trees stream in. The trade-off: layout shift when content arrives — reserve space (skeleton or `min-height`).
496
+
497
+ ### Server Components: parallel through composition
498
+
499
+ ```tsx
500
+ // INCORRECT — sibling awaits run sequentially inside one component
501
+ export default async function Page() {
502
+ const user = await getUser();
503
+ const cart = await getCart();
504
+ return <View user={user} cart={cart} />;
505
+ }
506
+
507
+ // CORRECT — split into children, React runs them in parallel
508
+ export default async function Page() {
509
+ return (
510
+ <View>
511
+ <UserSection />
512
+ <CartSection />
513
+ </View>
514
+ );
515
+ }
516
+ ```
517
+
518
+ ## 2. Bundle Size Optimization (CRITICAL)
519
+
520
+ ### Direct imports, not barrels
521
+
522
+ Barrel `index.ts` files force the bundler to walk the entire module graph even when tree-shaking removes most of it. Direct imports save 200-800ms of first-load JS in many real-world apps.
523
+
524
+ ```ts
525
+ // INCORRECT
526
+ import { Button, Card, Modal } from "@/components";
527
+
528
+ // CORRECT
529
+ import { Button } from "@/components/Button";
530
+ import { Card } from "@/components/Card";
531
+ import { Modal } from "@/components/Modal";
532
+ ```
533
+
534
+ Next.js 13.5+ has [Optimize Package Imports](https://nextjs.org/docs/app/api-reference/next-config-js/optimizePackageImports) that automates this for listed packages — use it; manual direct imports still required for non-listed libs.
535
+
536
+ ### Statically analyzable paths
537
+
538
+ ```ts
539
+ // INCORRECT — defeats bundler/trace analysis
540
+ const mod = await import(`./pages/${name}`);
541
+
542
+ // CORRECT — explicit per branch
543
+ const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
544
+ ```
545
+
546
+ ### Dynamic imports for heavy components
547
+
548
+ ```tsx
549
+ import dynamic from "next/dynamic";
550
+
551
+ const HeavyChart = dynamic(() => import("./HeavyChart"), {
552
+ loading: () => <Skeleton />,
553
+ ssr: false, // when client-only
554
+ });
555
+ ```
556
+
557
+ ### Defer third-party scripts
558
+
559
+ Load analytics, logging, support widgets AFTER hydration. Use `next/script` with `strategy="afterInteractive"` (default) or `"lazyOnload"`.
560
+
561
+ ### Conditional module loading
562
+
563
+ ```tsx
564
+ if (user.role === "admin") {
565
+ const { AdminPanel } = await import("./admin/AdminPanel");
566
+ // ...
567
+ }
568
+ ```
569
+
570
+ ### Preload on hover/focus
571
+
572
+ Trigger `<link rel="preload">` or `import()` on hover so the bundle is in cache by the time the user clicks.
573
+
574
+ ## 3. Server-Side Performance (HIGH)
575
+
576
+ ### Authenticate Server Actions like API routes
577
+
578
+ Every `"use server"` function is a public endpoint. Authenticate AND authorize inside the action — never rely on the calling Client Component's gating.
579
+
580
+ ```ts
581
+ "use server";
582
+ export async function deleteUser(formData: FormData) {
583
+ const session = await getSession();
584
+ if (!session?.user) throw new Error("Unauthorized");
585
+ const targetId = String(formData.get("id"));
586
+ if (session.user.role !== "admin" && session.user.id !== targetId) {
587
+ throw new Error("Forbidden");
588
+ }
589
+ await db.user.delete({ where: { id: targetId } });
590
+ }
591
+ ```
592
+
593
+ ### `React.cache()` for per-request deduplication
594
+
595
+ ```ts
596
+ import { cache } from "react";
597
+
598
+ export const getUser = cache(async (id: string) => {
599
+ return db.user.findUnique({ where: { id } });
600
+ });
601
+ ```
602
+
603
+ `React.cache` dedupes within a single request. Calling `getUser("1")` from three Server Components in the same render = one DB query.
604
+
605
+ ### LRU cache for cross-request data
606
+
607
+ For data that does NOT change per request (config, lookup tables), cache outside React with an LRU cache or `unstable_cache`.
608
+
609
+ ### Avoid duplicate serialization in RSC props
610
+
611
+ When a Server Component renders the same data into multiple Client Components, the data is serialized once per consumer. Lift the Client Component up and pass children.
612
+
613
+ ### Hoist static I/O to module scope
614
+
615
+ ```ts
616
+ // CORRECT — runs once at module load
617
+ const fontData = readFileSync(fontPath);
618
+
619
+ export async function Page() {
620
+ return <Banner font={fontData} />;
621
+ }
622
+ ```
623
+
624
+ ### No mutable module-level state in RSC/SSR
625
+
626
+ Module state on the server is shared across all requests — a race condition between users. Use request-scoped storage (`headers()`, `cookies()`, async context) instead.
627
+
628
+ ### Minimize data passed to Client Components
629
+
630
+ Only serialize what the Client needs. Strip fields, paginate, project columns at the DB layer.
631
+
632
+ ### Parallelize nested fetches with Promise.all per item
633
+
634
+ ```ts
635
+ const users = await getUsers();
636
+ const enriched = await Promise.all(
637
+ users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
638
+ );
639
+ ```
640
+
641
+ ### Use `after()` for non-blocking work
642
+
643
+ Next.js 15 `after()` runs work after the response is sent — logging, cache warming, analytics.
644
+
645
+ ```ts
646
+ import { after } from "next/server";
647
+ export async function GET() {
648
+ const data = await getData();
649
+ after(() => logAnalytics(data));
650
+ return Response.json(data);
651
+ }
652
+ ```
653
+
654
+ ## 4. Client-Side Data Fetching (MEDIUM-HIGH)
655
+
656
+ ### SWR / TanStack Query for deduplication
657
+
658
+ Multiple components calling `useUser(id)` should share one network request and one cache entry. Use SWR or TanStack Query — never roll your own `useEffect` + `fetch` for shared data.
659
+
660
+ ### Deduplicate global event listeners
661
+
662
+ ```tsx
663
+ // INCORRECT — every component adds its own
664
+ useEffect(() => {
665
+ window.addEventListener("scroll", handler);
666
+ return () => window.removeEventListener("scroll", handler);
667
+ }, []);
668
+
669
+ // CORRECT — single shared listener via a hook + global subject
670
+ const useScroll = createScrollHook(); // singleton subject under the hood
671
+ ```
672
+
673
+ ### Passive listeners for scroll
674
+
675
+ ```ts
676
+ window.addEventListener("scroll", handler, { passive: true });
677
+ ```
678
+
679
+ Improves scrolling smoothness; the listener cannot `preventDefault()`.
680
+
681
+ ### localStorage: version + minimize
682
+
683
+ - Always store a `version` field; bump on schema change and migrate or discard old data
684
+ - Keep payloads small — `localStorage` is synchronous and blocks main thread
685
+
686
+ ## 5. Re-render Optimization (MEDIUM)
687
+
688
+ ### Don't subscribe to state used only in callbacks
689
+
690
+ ```tsx
691
+ // INCORRECT — re-renders every time count changes
692
+ const count = useStore((s) => s.count);
693
+ const handler = () => doSomething(count);
694
+
695
+ // CORRECT — read once on call
696
+ const handler = () => {
697
+ const count = useStore.getState().count;
698
+ doSomething(count);
699
+ };
700
+ ```
701
+
702
+ ### Extract expensive work into memoized components
703
+
704
+ ```tsx
705
+ // CORRECT — child re-renders only when `items` changes
706
+ const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
707
+ return <Chart data={transform(items)} />;
708
+ });
709
+ ```
710
+
711
+ ### Hoist default non-primitive props
712
+
713
+ ```tsx
714
+ // INCORRECT — new array each render breaks memo
715
+ <List items={items ?? []} />
716
+
717
+ // CORRECT
718
+ const EMPTY: Item[] = [];
719
+ <List items={items ?? EMPTY} />
720
+ ```
721
+
722
+ ### Primitive dependencies in effects
723
+
724
+ ```tsx
725
+ // INCORRECT — new object identity every render
726
+ useEffect(() => {}, [{ id, name }]);
727
+
728
+ // CORRECT — primitives
729
+ useEffect(() => {}, [id, name]);
730
+ ```
731
+
732
+ ### Subscribe to derived booleans, not raw values
733
+
734
+ ```tsx
735
+ // INCORRECT — re-renders for any cart change
736
+ const cart = useStore((s) => s.cart);
737
+ const hasItems = cart.length > 0;
738
+
739
+ // CORRECT — re-renders only when emptiness flips
740
+ const hasItems = useStore((s) => s.cart.length > 0);
741
+ ```
742
+
743
+ ### Derive during render, never via `useEffect`
744
+
745
+ ```tsx
746
+ // INCORRECT
747
+ const [full, setFull] = useState("");
748
+ useEffect(() => setFull(`${first} ${last}`), [first, last]);
749
+
750
+ // CORRECT
751
+ const full = `${first} ${last}`;
752
+ ```
753
+
754
+ ### Functional `setState` for stable callbacks
755
+
756
+ ```tsx
757
+ // CORRECT
758
+ const increment = useCallback(() => setCount((c) => c + 1), []);
759
+ ```
760
+
761
+ ### Lazy state initializer for expensive values
762
+
763
+ ```tsx
764
+ const [tree] = useState(() => parseTree(largeInput));
765
+ ```
766
+
767
+ ### Avoid memo for simple primitives
768
+
769
+ `useMemo(() => x + 1, [x])` is overhead. Memo earns its keep on object identity and expensive computation.
770
+
771
+ ### Split hooks with independent deps
772
+
773
+ ```tsx
774
+ // INCORRECT — both selectors re-run if either source changes
775
+ const { a, b } = useSomething(source1, source2);
776
+
777
+ // CORRECT
778
+ const a = useA(source1);
779
+ const b = useB(source2);
780
+ ```
781
+
782
+ ### Move interaction logic into event handlers
783
+
784
+ Event handlers run only on the user action — `useEffect` re-runs whenever deps change.
785
+
786
+ ### `startTransition` for non-urgent updates
787
+
788
+ ```tsx
789
+ const [pending, startTransition] = useTransition();
790
+ startTransition(() => setFilters(newFilters));
791
+ ```
792
+
793
+ ### `useDeferredValue` for expensive renders
794
+
795
+ ```tsx
796
+ const deferredQuery = useDeferredValue(query);
797
+ const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
798
+ ```
799
+
800
+ ### `useRef` for transient frequent values
801
+
802
+ For values that change often but should not trigger re-render (timestamps, last-key, accumulators).
803
+
804
+ ### Don't define components inside components
805
+
806
+ ```tsx
807
+ // INCORRECT — Inner is a new component on every Outer render
808
+ function Outer() {
809
+ const Inner = () => <span />;
810
+ return <Inner />;
811
+ }
812
+ ```
813
+
814
+ Each render makes a new `Inner` type, defeating reconciliation and unmounting children.
815
+
816
+ ## 6. Rendering Performance (MEDIUM)
817
+
818
+ ### Animate the wrapper, not the SVG
819
+
820
+ Transforming a `<div>` wrapper around an SVG is GPU-accelerated; transforming the SVG itself triggers paint.
821
+
822
+ ### `content-visibility: auto` for long lists
823
+
824
+ ```css
825
+ .row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
826
+ ```
827
+
828
+ Browser skips offscreen rendering — major win for lists with hundreds of rows.
829
+
830
+ ### Hoist static JSX
831
+
832
+ ```tsx
833
+ const STATIC_HEADER = <h1>Title</h1>;
834
+ function Page() {
835
+ return <>{STATIC_HEADER}<Body /></>;
836
+ }
837
+ ```
838
+
839
+ ### SVG: reduce coordinate precision
840
+
841
+ `d="M10.123456,20.654321"` → `d="M10.12,20.65"`. Each digit costs bytes; the visual difference is sub-pixel.
842
+
843
+ ### Hydration no-flicker via inline script
844
+
845
+ For values needed before hydration (theme, locale), inline a `<script>` that sets `document.documentElement.dataset.*` before React mounts.
846
+
847
+ ### Suppress expected hydration mismatches narrowly
848
+
849
+ ```tsx
850
+ <time suppressHydrationWarning>{new Date().toLocaleString()}</time>
851
+ ```
852
+
853
+ Use ONLY for known-divergent leaf nodes — never on a tree containing other children.
854
+
855
+ ### `<Activity>` for show/hide instead of mount/unmount
856
+
857
+ React 19 `<Activity mode="visible|hidden">` keeps tree state and effects mounted but hides — cheaper than unmount/remount for tabs and accordions.
858
+
859
+ ### Ternary over `&&` for conditional render
860
+
861
+ ```tsx
862
+ // INCORRECT — `0` renders as text node
863
+ {count && <Badge>{count}</Badge>}
864
+
865
+ // CORRECT
866
+ {count > 0 ? <Badge>{count}</Badge> : null}
867
+ ```
868
+
869
+ ### `useTransition` for loading states
870
+
871
+ Pair `startTransition` with the action; React shows the previous UI as `isPending` while the next state computes.
872
+
873
+ ### React DOM resource hints
874
+
875
+ ```tsx
876
+ import { preload, preconnect } from "react-dom";
877
+ preload("/api/critical", { as: "fetch" });
878
+ preconnect("https://api.example.com");
879
+ ```
880
+
881
+ ### `defer` / `async` on `<script>` tags
882
+
883
+ `defer` for ordered execution after DOMContentLoaded; `async` for fire-and-forget.
884
+
885
+ ## 7. JavaScript Performance (LOW-MEDIUM)
886
+
887
+ - **Batch DOM/CSS changes** — apply via class swap or `cssText`, not property-by-property
888
+ - **`Map` for repeated lookups** — `O(1)` vs `O(n)` linear scan
889
+ - **Cache property access in loops** — `const len = arr.length`
890
+ - **Memoize pure functions** — module-level `Map<key, result>`
891
+ - **Cache `localStorage` reads** — sync API; one read per render
892
+ - **Combine `filter().map()` into one pass** — `flatMap` or single `for`
893
+ - **Check array length first** before expensive comparisons
894
+ - **Early return** from functions
895
+ - **Hoist RegExp** out of loops — compilation is not free
896
+ - **Loop for min/max** instead of `sort()` — `O(n)` vs `O(n log n)`
897
+ - **`Set`/`Map` for membership** — `O(1)` vs `Array.includes` `O(n)`
898
+ - **`toSorted()` over mutation** when immutability matters
899
+ - **`flatMap` to map and filter in one pass**
900
+ - **`requestIdleCallback`** for non-critical work
901
+
902
+ ## 8. Advanced Patterns (LOW)
903
+
904
+ ### `useEffectEvent` deps
905
+
906
+ Values from `useEffectEvent` are stable — do NOT add them to effect deps.
907
+
908
+ ### Event handler refs
909
+
910
+ For stable callbacks passed to memoized children:
911
+
912
+ ```tsx
913
+ const handlerRef = useRef(handler);
914
+ useEffect(() => { handlerRef.current = handler; });
915
+ const stable = useCallback((arg) => handlerRef.current(arg), []);
916
+ ```
917
+
918
+ ### Init once per app load
919
+
920
+ For module-level singletons (telemetry, logger), guard with a module-scope flag — not `useEffect`.
921
+
922
+ ### `useLatest` for stable callback refs
923
+
924
+ ```tsx
925
+ function useLatest<T>(value: T) {
926
+ const ref = useRef(value);
927
+ ref.current = value;
928
+ return ref;
929
+ }
930
+ ```
931
+
932
+ ## Automated Tools
933
+
934
+ Many of these rules are now automated:
935
+
936
+ - **Next.js 13.5+ Optimize Package Imports** — barrel import optimization
937
+ - **React Compiler** (RFC, in canary) — auto-memoization
938
+ - **Turbopack** — faster builds, better tree-shaking
939
+ - **Bundle Analyzer** (`@next/bundle-analyzer`) — visualize first-load JS
940
+
941
+ When the project ships React Compiler, demote `rerender-*` manual memoization rules to "review-only" — the compiler handles them. Manual `useMemo`/`useCallback` becomes unnecessary noise.
942
+
943
+ ## Lighthouse / Web Vitals Mapping
944
+
945
+ | Metric | Most relevant categories |
946
+ |---|---|
947
+ | **LCP** (Largest Contentful Paint) | Waterfalls, Bundle Size, Resource Hints |
948
+ | **INP** (Interaction to Next Paint) | Re-render, Rendering, JavaScript |
949
+ | **CLS** (Cumulative Layout Shift) | Rendering (Suspense placement, image dimensions) |
950
+ | **TBT** (Total Blocking Time) | Bundle Size, JavaScript, Defer Third-Party |
951
+ | **FID** (legacy) | Bundle Size, Hydration |
952
+
953
+ ## Related
954
+
955
+ - Skills: [react-patterns](../react-patterns/SKILL.md), [react-testing](../react-testing/SKILL.md), [frontend-patterns](../frontend-patterns/SKILL.md), [accessibility](../accessibility/SKILL.md), [nextjs-turbopack](../nextjs-turbopack/SKILL.md)
956
+ - Rules: [rules/react/](../../rules/react/)
957
+ - Agents: `react-reviewer` enforces these rules in code review; `react-build-resolver` handles related build failures
958
+ - Commands: `/react-review`, `/react-build`, `/react-test`
959
+
960
+ ## Attribution
961
+
962
+ Adapted from Vercel Labs `react-best-practices` skill (MIT License, copyright Vercel Engineering, v1.0.0 January 2026). Source: [https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices](https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices).
963
+
964
+ This skill restructures and adapts the original 70-rule catalog into a single navigable reference. For the full original ruleset with extended examples, see the upstream repository.
965
+
966
+ ### react-testing
967
+
968
+ Use when writing or fixing tests for React components, hooks, or pages — React Testing Library query priority, userEvent, async assertions, MSW network mocking, accessibility assertions with axe, and the RTL-vs-Playwright decision boundary.
969
+
970
+ # React Testing
971
+
972
+ Comprehensive React testing patterns for behavior-focused component tests, custom hook tests, accessibility assertions, and network-level mocking.
973
+
974
+ ## When to Activate
975
+
976
+ - Writing tests for React components, custom hooks, or pages
977
+ - Adding test coverage to legacy untested components
978
+ - Migrating from Enzyme or class-component-era patterns to React Testing Library
979
+ - Setting up Vitest or Jest for a new React project
980
+ - Mocking HTTP requests in tests
981
+ - Asserting accessibility violations
982
+ - Deciding which tests belong in RTL vs Playwright Component Testing vs full E2E
983
+
984
+ ## Core Principle
985
+
986
+ Test what the user sees and does, not implementation details.
987
+
988
+ A test should:
989
+
990
+ - Render the component with the same providers it has in production
991
+ - Interact with it via accessible queries (role, label) and `userEvent`
992
+ - Assert visible output and observable side effects (callback fired, request sent)
993
+
994
+ A test should NOT:
995
+
996
+ - Inspect component state, props passed to children, or which hooks were called
997
+ - Mock React itself or framework hooks
998
+ - Assert on the number of renders or DOM structure beyond what affects users
999
+
1000
+ ## Library Choice
1001
+
1002
+ | Runner | When | Note |
1003
+ |---|---|---|
1004
+ | **Vitest** | Vite, Remix, modern setups | Faster, native ESM, Jest-compatible API |
1005
+ | **Jest** | Next.js, CRA, established repos | Default for many React projects |
1006
+ | **Playwright Component Testing** | Real browser engine needed | Use when JSDOM lacks the required feature |
1007
+ | **Cypress Component Testing** | Real browser, Cypress already in use | Alternative to Playwright CT |
1008
+
1009
+ Pick one. Do not run RTL + Vitest AND Playwright CT in the same repo unless you have a clear lane separation.
1010
+
1011
+ ## Query Priority
1012
+
1013
+ React Testing Library exposes queries in three tiers — use top-down:
1014
+
1015
+ 1. **Accessible to everyone**: `getByRole`, `getByLabelText`, `getByPlaceholderText`, `getByText`, `getByDisplayValue`
1016
+ 2. **Semantic**: `getByAltText`, `getByTitle`
1017
+ 3. **Test IDs (escape hatch)**: `getByTestId`
1018
+
1019
+ ```tsx
1020
+ // Best
1021
+ screen.getByRole("button", { name: /save/i });
1022
+
1023
+ // OK for inputs
1024
+ screen.getByLabelText("Email");
1025
+
1026
+ // Last resort
1027
+ screen.getByTestId("save-btn");
1028
+ ```
1029
+
1030
+ Variants:
1031
+
1032
+ - `getBy*` — throws if no match
1033
+ - `queryBy*` — returns `null` (use for "assert absence")
1034
+ - `findBy*` — async, returns a Promise (use for elements that appear after async work)
1035
+
1036
+ ## User Interaction with `userEvent`
1037
+
1038
+ ```tsx
1039
+ import userEvent from "@testing-library/user-event";
1040
+
1041
+ test("submits the form", async () => {
1042
+ const user = userEvent.setup();
1043
+ const onSubmit = vi.fn();
1044
+ render(<UserForm onSubmit={onSubmit} />);
1045
+
1046
+ await user.type(screen.getByLabelText("Email"), "user@example.com");
1047
+ await user.click(screen.getByRole("button", { name: /save/i }));
1048
+
1049
+ expect(onSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
1050
+ });
1051
+ ```
1052
+
1053
+ - Always `await` userEvent calls
1054
+ - Call `userEvent.setup()` once per test, reuse the returned `user`
1055
+ - `userEvent` simulates a real browser sequence; `fireEvent` dispatches a single synthetic event — prefer `userEvent`
1056
+
1057
+ ## Async Patterns
1058
+
1059
+ ```tsx
1060
+ // Element that appears after async work
1061
+ expect(await screen.findByText("Loaded")).toBeInTheDocument();
1062
+
1063
+ // Side effect assertion
1064
+ await waitFor(() => expect(saveSpy).toHaveBeenCalled());
1065
+
1066
+ // Element that should disappear
1067
+ await waitForElementToBeRemoved(() => screen.queryByText("Loading"));
1068
+ ```
1069
+
1070
+ Never `setTimeout` + assertion — flaky. Use the matchers above.
1071
+
1072
+ ## Network Mocking with MSW
1073
+
1074
+ Mock Service Worker mocks at the network layer. The component, hooks, and fetch library all behave exactly as in production.
1075
+
1076
+ ### Setup
1077
+
1078
+ ```ts
1079
+ // test/setup.ts
1080
+ import { setupServer } from "msw/node";
1081
+ import { http, HttpResponse } from "msw";
1082
+
1083
+ export const handlers = [
1084
+ http.get("/api/users/:id", ({ params }) =>
1085
+ HttpResponse.json({ id: params.id, name: "Alice" }),
1086
+ ),
1087
+ http.post("/api/users", async ({ request }) => {
1088
+ const body = await request.json();
1089
+ return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
1090
+ }),
1091
+ ];
1092
+
1093
+ export const server = setupServer(...handlers);
1094
+
1095
+ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
1096
+ afterEach(() => server.resetHandlers());
1097
+ afterAll(() => server.close());
1098
+ ```
1099
+
1100
+ Configure `onUnhandledRequest: "error"` so any unmocked request fails the test loudly — silent passes are worse than red.
1101
+
1102
+ ### Per-test override
1103
+
1104
+ ```tsx
1105
+ test("renders error on 500", async () => {
1106
+ server.use(
1107
+ http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
1108
+ );
1109
+ render(<UserPage id="1" />);
1110
+ expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
1111
+ });
1112
+ ```
1113
+
1114
+ ## Provider Wrapping
1115
+
1116
+ Wrap providers once in a `test-utils.tsx`:
1117
+
1118
+ ```tsx
1119
+ // test-utils.tsx
1120
+ import { render, RenderOptions } from "@testing-library/react";
1121
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1122
+
1123
+ export function renderWithProviders(
1124
+ ui: React.ReactElement,
1125
+ options?: RenderOptions,
1126
+ ) {
1127
+ const queryClient = new QueryClient({
1128
+ defaultOptions: { queries: { retry: false } },
1129
+ });
1130
+
1131
+ return render(
1132
+ <QueryClientProvider client={queryClient}>
1133
+ <ThemeProvider theme={lightTheme}>
1134
+ <MemoryRouter>{ui}</MemoryRouter>
1135
+ </ThemeProvider>
1136
+ </QueryClientProvider>,
1137
+ options,
1138
+ );
1139
+ }
1140
+
1141
+ export * from "@testing-library/react";
1142
+ ```
1143
+
1144
+ Then `import { renderWithProviders, screen } from "test-utils"` in every test file.
1145
+
1146
+ ## Custom Hook Testing
1147
+
1148
+ ```tsx
1149
+ import { renderHook, act } from "@testing-library/react";
1150
+
1151
+ test("useCounter increments and decrements", () => {
1152
+ const { result } = renderHook(() => useCounter(0));
1153
+
1154
+ expect(result.current.count).toBe(0);
1155
+
1156
+ act(() => result.current.increment());
1157
+ expect(result.current.count).toBe(1);
1158
+
1159
+ act(() => result.current.decrement());
1160
+ expect(result.current.count).toBe(0);
1161
+ });
1162
+
1163
+ test("useCounter accepts initial value", () => {
1164
+ const { result } = renderHook(() => useCounter(10));
1165
+ expect(result.current.count).toBe(10);
1166
+ });
1167
+
1168
+ test("useUser fetches user data", async () => {
1169
+ // Instantiate QueryClient ONCE per test outside the wrapper so it survives re-renders.
1170
+ // Creating it inside the wrapper closure resets cache state on every render, producing flaky tests.
1171
+ const queryClient = new QueryClient({
1172
+ defaultOptions: { queries: { retry: false } },
1173
+ });
1174
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
1175
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
1176
+ );
1177
+
1178
+ const { result } = renderHook(() => useUser("1"), { wrapper });
1179
+
1180
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
1181
+ expect(result.current.data).toEqual({ id: "1", name: "Alice" });
1182
+ });
1183
+ ```
1184
+
1185
+ - Wrap state-changing calls in `act`
1186
+ - Test through the hook's public API only
1187
+ - For hooks that use context, pass a `wrapper`
1188
+
1189
+ ## Accessibility Assertions
1190
+
1191
+ ```tsx
1192
+ import { axe, toHaveNoViolations } from "jest-axe"; // or vitest-axe
1193
+ expect.extend(toHaveNoViolations);
1194
+
1195
+ test("UserCard has no a11y violations", async () => {
1196
+ const { container } = render(<UserCard user={mockUser} />);
1197
+ expect(await axe(container)).toHaveNoViolations();
1198
+ });
1199
+ ```
1200
+
1201
+ Run axe in component tests for every interactive component. Catches:
1202
+
1203
+ - Missing labels on form inputs
1204
+ - Invalid ARIA usage
1205
+ - Poor color contrast (limited — JSDOM has no real CSS engine, so this works for inline styles only; visual contrast belongs in Playwright)
1206
+ - Missing alt text on images
1207
+ - Heading order violations
1208
+
1209
+ Cross-link: [skills/accessibility/SKILL.md](../accessibility/SKILL.md) for the broader a11y testing playbook.
1210
+
1211
+ ## When NOT to Use Snapshot Tests
1212
+
1213
+ Snapshots of rendered output:
1214
+
1215
+ - Break on every styling change
1216
+ - Get rubber-stamped during review
1217
+ - Test implementation detail (DOM structure), not behavior
1218
+
1219
+ Acceptable snapshot uses:
1220
+
1221
+ - Pure data serialization functions (`formatInvoice(invoice)` -> stable string)
1222
+ - Generated config files (e.g., webpack config output)
1223
+
1224
+ For visual regression on components, use Playwright/Cypress screenshots or Percy/Chromatic — actual visual diffs, not DOM strings.
1225
+
1226
+ ## When to Reach for Playwright / Cypress
1227
+
1228
+ JSDOM (used by Vitest/Jest) cannot:
1229
+
1230
+ - Render real layout (flexbox, grid, viewport queries)
1231
+ - Run native browser animation, CSS transitions
1232
+ - Test scrolling behavior, drag-and-drop, paste from clipboard
1233
+ - Handle iframes, popups, downloads, cross-origin flows
1234
+ - Run real network in a controlled environment with full DevTools support
1235
+
1236
+ For any of those, use Playwright Component Testing (component test in real browser) or full E2E. See [e2e-testing skill](../e2e-testing/SKILL.md).
1237
+
1238
+ Decision boundary:
1239
+
1240
+ - A hook, a presentational component, a form with logic -> RTL
1241
+ - A component whose layout matters or that uses browser APIs not in JSDOM -> Playwright CT
1242
+ - A full user flow across multiple pages -> Playwright/Cypress E2E
1243
+
1244
+ ## Coverage Targets
1245
+
1246
+ | Layer | Target |
1247
+ |---|---|
1248
+ | Pure utilities | >=90% |
1249
+ | Custom hooks | >=85% |
1250
+ | Presentational components | >=80% — behavior, not lines |
1251
+ | Container components | >=70% — golden paths + error states |
1252
+ | Pages | E2E covered separately; smoke test minimum |
1253
+
1254
+ Configure via `vitest.config.ts` / `jest.config.js`:
1255
+
1256
+ ```ts
1257
+ // vitest.config.ts
1258
+ test: {
1259
+ coverage: {
1260
+ provider: "v8",
1261
+ reporter: ["text", "html", "lcov"],
1262
+ thresholds: {
1263
+ lines: 80,
1264
+ functions: 80,
1265
+ branches: 70,
1266
+ statements: 80,
1267
+ },
1268
+ },
1269
+ }
1270
+ ```
1271
+
1272
+ ## Anti-Patterns
1273
+
1274
+ - `container.querySelector("...")` — bypasses accessibility queries, lets tests pass when real users would fail
1275
+ - Asserting on number of renders — implementation detail
1276
+ - `jest.mock("react", ...)` — never mock React. Refactor the component instead
1277
+ - Mocking child components by default — tests the integration, not isolation. Mock only when the child has heavy side effects
1278
+ - Ignoring `act()` warnings — they signal real bugs (state update after unmount, missing async wrapping)
1279
+ - Sharing mutable state across tests — flakes when test order changes
1280
+ - Tests that pass with `it.skip()` removed — your test does not actually assert what you think
1281
+
1282
+ ## TDD Workflow
1283
+
1284
+ ```
1285
+ RED -> Write failing test for the next requirement
1286
+ GREEN -> Write minimal component code to pass
1287
+ REFACTOR -> Improve the component, tests stay green
1288
+ REPEAT -> Next requirement
1289
+ ```
1290
+
1291
+ For new components:
1292
+
1293
+ 1. Define the component's prop type and signature
1294
+ 2. Write the first test for the simplest case
1295
+ 3. Verify it fails for the right reason
1296
+ 4. Implement just enough to pass
1297
+ 5. Add the next test case
1298
+ 6. Refactor when the third similar test reveals a pattern
1299
+
1300
+ ## Test Commands
1301
+
1302
+ ```bash
1303
+ # Vitest
1304
+ vitest # watch
1305
+ vitest run # one-shot
1306
+ vitest run --coverage # with coverage
1307
+ vitest run path/to/file.test.tsx # single file
1308
+
1309
+ # Jest
1310
+ jest --watch
1311
+ jest --coverage
1312
+ jest path/to/file.test.tsx
1313
+
1314
+ # CI mode
1315
+ CI=true vitest run --coverage
1316
+ ```
1317
+
1318
+ ## Related
1319
+
1320
+ - Rules: [rules/react/testing.md](../../rules/react/testing.md)
1321
+ - Skills: [react-patterns](../react-patterns/SKILL.md), [accessibility](../accessibility/SKILL.md), [e2e-testing](../e2e-testing/SKILL.md), [tdd-workflow](../tdd-workflow/SKILL.md)
1322
+ - Agents: `react-reviewer` (reviews test quality during code review), `tdd-guide` (enforces TDD process)
1323
+ - Commands: `/react-test`, `/react-review`
1324
+
1325
+ ## Examples
1326
+
1327
+ ### Form submission with MSW and userEvent
1328
+
1329
+ ```tsx
1330
+ test("submits user form and shows success", async () => {
1331
+ server.use(
1332
+ http.post("/api/users", () =>
1333
+ HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
1334
+ ),
1335
+ );
1336
+
1337
+ const user = userEvent.setup();
1338
+ renderWithProviders(<UserForm />);
1339
+
1340
+ await user.type(screen.getByLabelText("Name"), "Alice");
1341
+ await user.type(screen.getByLabelText("Email"), "alice@example.com");
1342
+ await user.click(screen.getByRole("button", { name: /save/i }));
1343
+
1344
+ expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
1345
+ });
1346
+ ```
1347
+
1348
+ ### Testing an error boundary
1349
+
1350
+ ```tsx
1351
+ function Broken() {
1352
+ throw new Error("boom");
1353
+ }
1354
+
1355
+ test("error boundary renders fallback", () => {
1356
+ // Suppress React's console.error noise for the expected throw, then restore so
1357
+ // the spy does not leak across tests and hide real errors elsewhere.
1358
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1359
+ try {
1360
+ render(
1361
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
1362
+ <Broken />
1363
+ </ErrorBoundary>,
1364
+ );
1365
+
1366
+ expect(screen.getByText("Something went wrong")).toBeInTheDocument();
1367
+ } finally {
1368
+ errorSpy.mockRestore();
1369
+ }
1370
+ });
1371
+ ```
1372
+
1373
+ ### Testing a Suspense boundary
1374
+
1375
+ ```tsx
1376
+ test("shows loading then content", async () => {
1377
+ renderWithProviders(
1378
+ <Suspense fallback={<div>Loading...</div>}>
1379
+ <UserDetail id="1" />
1380
+ </Suspense>,
1381
+ );
1382
+
1383
+ expect(screen.getByText("Loading...")).toBeInTheDocument();
1384
+ expect(await screen.findByText("Alice")).toBeInTheDocument();
1385
+ });
1386
+ ```