@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
@@ -1,55 +1,931 @@
1
1
  # AGENTS.md
2
2
 
3
- <!-- generated from workflow-pack typescript-rules v1.4.0 -->
3
+ <!-- generated from workflow-pack typescript-rules v2.0.0 -->
4
4
  <!-- adapted from affaan-m/ECC (MIT) — https://github.com/affaan-m/ECC -->
5
5
 
6
- ## TypeScript/JavaScript conventions (beyond what the linter enforces)
6
+ ## TypeScript/JavaScript + React conventions (beyond what the linter enforces)
7
7
 
8
- `eslint` and `prettier` already enforce formatting, `no-explicit-any`, and stray `console.log` — this pack is the judgment they can't encode. Keep the mechanical checks in CI (`prettier --check .`, `eslint .`); the reviewer covers the rest.
8
+ `eslint` and `prettier` already enforce formatting, `no-explicit-any`, and stray `console.log` — this pack is the judgment they can't encode. Keep the mechanical checks in CI (`prettier --check .`, `eslint .`); the reviewers below cover the rest.
9
9
 
10
- ### Types at the surface
10
+ ## Types and interfaces
11
11
 
12
- - Give exported functions, shared utilities, and public methods explicit parameter and return types; let inference handle obvious locals.
13
- - Use `interface` for object shapes meant to be extended; use `type` for unions, intersections, and mapped types. Prefer string-literal unions over `enum` unless interop demands one.
14
- - Use `unknown` for external/untrusted input and narrow it before use (`error instanceof Error`); reserve `any` for genuine escape hatches, never as the default.
15
- - Type React props with a named `interface`/`type` and type callback props explicitly; skip `React.FC` unless you need it.
12
+ ### Public APIs
16
13
 
17
- ### Immutability
14
+ - Add parameter and return types to exported functions, shared utilities, and public class methods.
15
+ - Let TypeScript infer obvious local variable types.
16
+ - Extract repeated inline object shapes into named types or interfaces.
18
17
 
19
- - Return new objects/arrays via spread (`{ ...user, name }`) or `map`/`filter` — never `push`/`splice`/assignment on a value you received.
18
+ ```typescript
19
+ // WRONG: exported function without explicit types
20
+ export function formatUser(user) {
21
+ return `${user.firstName} ${user.lastName}`
22
+ }
20
23
 
21
- ### Errors and boundaries
24
+ // CORRECT: explicit types on public APIs
25
+ interface User {
26
+ firstName: string
27
+ lastName: string
28
+ }
22
29
 
23
- - Handle errors explicitly with context, then re-raise or return a typed error; narrow an `unknown` caught error before reading `.message`.
24
- - Validate external input at the boundary with a schema (e.g. Zod) and infer the type from the schema rather than hand-writing both.
25
- - Read required secrets from the environment and fail fast at startup if one is missing.
30
+ export function formatUser(user: User): string {
31
+ return `${user.firstName} ${user.lastName}`
32
+ }
33
+ ```
26
34
 
27
- ### Flag in review (a linter won't)
35
+ ### Interfaces vs. type aliases
36
+
37
+ - Use `interface` for object shapes that may be extended or implemented.
38
+ - Use `type` for unions, intersections, tuples, mapped types, and utility types.
39
+ - Prefer string-literal unions over `enum` unless an `enum` is required for interop.
40
+
41
+ ```typescript
42
+ interface User {
43
+ id: string
44
+ email: string
45
+ }
46
+
47
+ type UserRole = 'admin' | 'member'
48
+ type UserWithRole = User & { role: UserRole }
49
+ ```
50
+
51
+ ### Avoid `any`
52
+
53
+ - Avoid `any` in application code.
54
+ - Use `unknown` for external or untrusted input, then narrow it safely.
55
+ - Use generics when a value's type depends on the caller.
56
+
57
+ ```typescript
58
+ // WRONG: any removes type safety
59
+ function getErrorMessage(error: any) {
60
+ return error.message
61
+ }
62
+
63
+ // CORRECT: unknown forces safe narrowing
64
+ function getErrorMessage(error: unknown): string {
65
+ if (error instanceof Error) {
66
+ return error.message
67
+ }
68
+ return 'Unexpected error'
69
+ }
70
+ ```
71
+
72
+ ### React props
73
+
74
+ - Define component props with a named `interface` or `type`; type callback props explicitly.
75
+ - Prefer `type Props = {}` for closed component prop shapes; use `interface` only when the prop type is extended via declaration merging or exported as a public API extension point.
76
+ - Do not use `React.FC` unless there is a specific reason to.
77
+ - Always destructure props in the parameter list — no `props.user` access inside the body.
78
+
79
+ ```tsx
80
+ type Props = {
81
+ user: User;
82
+ onSelect: (id: string) => void;
83
+ };
84
+
85
+ export function UserCard({ user, onSelect }: Props) {
86
+ return (
87
+ <button type="button" onClick={() => onSelect(user.id)}>
88
+ {user.name}
89
+ </button>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ### JavaScript files
95
+
96
+ - In `.js`/`.jsx` files, use JSDoc when types improve clarity and a TypeScript migration isn't practical; keep JSDoc aligned with runtime behavior.
97
+
98
+ ## Immutability
99
+
100
+ Return new objects/arrays via spread or `map`/`filter` — never mutate an argument you were handed. Shared references make in-place mutation a spooky-action-at-a-distance bug: some other holder of the same object sees a change it never asked for.
101
+
102
+ ```typescript
103
+ interface User {
104
+ id: string
105
+ name: string
106
+ }
107
+
108
+ // WRONG — mutates the caller's object
109
+ function updateUser(user: User, name: string): User {
110
+ user.name = name
111
+ return user
112
+ }
113
+
114
+ // CORRECT — returns a new object, original untouched
115
+ function updateUser(user: Readonly<User>, name: string): User {
116
+ return { ...user, name }
117
+ }
118
+ ```
119
+
120
+ - **Object field:** `{ ...obj, field: value }`.
121
+ - **Nested field:** spread each level you touch — `{ ...obj, addr: { ...obj.addr, city } }`.
122
+ - **Array append/remove:** `[...list, item]`, `list.filter(x => x.id !== id)`, `list.map(x => x.id === id ? { ...x, done: true } : x)` — not `push`/`splice`/index assignment.
123
+ - **Record update:** `{ ...map, [key]: value }` and `const { [key]: _drop, ...rest } = map` to remove a key.
124
+
125
+ A locally-scoped object that never escapes the function — a builder/accumulator you allocated and return once — is fine to mutate. The rule protects values that are *shared*.
126
+
127
+ ## Error handling
128
+
129
+ Use `async`/`await` with `try`/`catch` and narrow `unknown` errors before reading them.
130
+
131
+ ```typescript
132
+ function getErrorMessage(error: unknown): string {
133
+ if (error instanceof Error) return error.message
134
+ return 'Unexpected error'
135
+ }
136
+
137
+ async function loadUser(userId: string): Promise<User> {
138
+ try {
139
+ return await riskyOperation(userId)
140
+ } catch (error: unknown) {
141
+ logger.error('Operation failed', error)
142
+ throw new Error(getErrorMessage(error))
143
+ }
144
+ }
145
+ ```
146
+
147
+ - Handle errors explicitly with context, then re-raise or return a typed error.
148
+ - Empty `catch` blocks that swallow the error are a defect, not a shortcut.
149
+ - `JSON.parse` throws on invalid input — always wrap it.
150
+ - Always `throw new Error("message")`, never `throw "message"`.
151
+ - Wrap React trees that fetch or do async work in an `<ErrorBoundary>`.
152
+
153
+ ## Input validation
154
+
155
+ Validate external input at the boundary with a schema (Zod) and infer the type from the schema rather than hand-writing both.
156
+
157
+ ```typescript
158
+ import { z } from 'zod'
159
+
160
+ const userSchema = z.object({
161
+ email: z.string().email(),
162
+ age: z.number().int().min(0).max(150)
163
+ })
164
+
165
+ type UserInput = z.infer<typeof userSchema>
166
+ const validated: UserInput = userSchema.parse(input)
167
+ ```
168
+
169
+ Read required secrets from the environment and fail fast at startup if one is missing.
170
+
171
+ ## Console/logging
172
+
173
+ No `console.log` statements in production code — use a structured logger.
174
+
175
+ ## Common patterns
176
+
177
+ ### API response format
178
+
179
+ ```typescript
180
+ interface ApiResponse<T> {
181
+ success: boolean
182
+ data?: T
183
+ error?: string
184
+ meta?: { total: number; page: number; limit: number }
185
+ }
186
+ ```
187
+
188
+ ### Repository pattern
189
+
190
+ ```typescript
191
+ interface Repository<T> {
192
+ findAll(filters?: Filters): Promise<T[]>
193
+ findById(id: string): Promise<T | null>
194
+ create(data: CreateDto): Promise<T>
195
+ update(id: string, data: UpdateDto): Promise<T>
196
+ delete(id: string): Promise<void>
197
+ }
198
+ ```
199
+
200
+ ### Custom hooks
201
+
202
+ ```typescript
203
+ export function useDebounce<T>(value: T, delay: number): T {
204
+ const [debouncedValue, setDebouncedValue] = useState<T>(value)
205
+
206
+ useEffect(() => {
207
+ const handler = setTimeout(() => setDebouncedValue(value), delay)
208
+ return () => clearTimeout(handler)
209
+ }, [value, delay])
210
+
211
+ return debouncedValue
212
+ }
213
+ ```
214
+
215
+ ## Secret management
216
+
217
+ ```typescript
218
+ // NEVER: hardcoded secrets
219
+ const apiKey = "sk-proj-xxxxx"
220
+
221
+ // ALWAYS: environment variables, fail fast if missing
222
+ const apiKey = process.env.API_KEY
223
+ if (!apiKey) {
224
+ throw new Error('API_KEY not configured')
225
+ }
226
+ ```
227
+
228
+ ## Testing (TypeScript baseline)
229
+
230
+ Use **Playwright** for E2E tests covering critical user flows. React-specific component/hook testing patterns are covered in the [react-testing skill](#) below.
231
+
232
+ ---
233
+
234
+ # React coding style
235
+
236
+ ## File extensions
237
+
238
+ - `.tsx` for any file containing JSX, even a one-liner.
239
+ - `.ts` for pure logic, custom hooks without JSX, type definitions, utilities.
240
+ - `.test.tsx` / `.test.ts` mirroring the source file.
241
+ - Use `.jsx` only when the project intentionally avoids TypeScript — flag every new untyped React file in review.
242
+
243
+ ## Naming
244
+
245
+ - Components: `PascalCase` for both the symbol and the file (`UserCard.tsx`, default export `UserCard`).
246
+ - Custom hooks: `useCamelCase` for the symbol, kebab-case for the file when that's the project convention (`use-debounce.ts` exports `useDebounce`).
247
+ - Context: `<Domain>Context` symbol, `<Domain>Provider` provider component, `use<Domain>` consumer hook.
248
+ - Event handlers: `handleClick`, `handleSubmit` inside the component; the prop that receives it is `onClick`, `onSubmit`.
249
+ - Boolean props: `isLoading`, `hasError`, `canSubmit` — never `loading` or `error` alone for booleans.
250
+
251
+ ## JSX
252
+
253
+ - Self-close tags with no children: `<img />`, `<UserCard user={u} />`.
254
+ - Use fragments `<>...</>` over a wrapper `<div>` when no DOM element is needed.
255
+ - Conditional rendering: `{condition && <Foo />}` for booleans, ternary for either/or, early return for guard clauses. **Use a ternary, not `&&`, when the left side can be `0`** — `{count && <Badge>{count}</Badge>}` renders the literal text `0`; use `{count > 0 ? <Badge>{count}</Badge> : null}`.
256
+ - Never put logic inline in JSX when it reads as multi-line — extract to a const above the return or a function.
257
+
258
+ ```tsx
259
+ // Prefer
260
+ const greeting = user.isAdmin ? "Welcome, admin" : `Hello ${user.name}`;
261
+ return <h1>{greeting}</h1>;
262
+
263
+ // Over
264
+ return <h1>{user.isAdmin ? "Welcome, admin" : `Hello ${user.name}`}</h1>;
265
+ ```
266
+
267
+ ## Imports
268
+
269
+ - React imports first, then third-party libs, then absolute project imports, then relative.
270
+ - Type-only imports: `import type { ReactNode } from "react"` — never mix runtime and type imports in one statement when `consistent-type-imports` is configured.
271
+
272
+ ## Hooks discipline
273
+
274
+ - Custom hooks must start with `use` — enforced by `eslint-plugin-react-hooks`.
275
+ - Hooks are top-level only, never conditional (not inside `if`/`for`/`&&`/ternary, not after an early return).
276
+ - Group all hook calls at the top of the component, before any conditional logic.
277
+ - Cleanup every subscription, interval, and listener.
278
+ - Use the functional updater (`setX(prev => prev + 1)`) when new state depends on old state.
279
+ - Default position: do not memoize — add `useMemo`/`useCallback` only when a profiler or a dependency chain proves it matters.
280
+ - Avoid creating ad-hoc hooks for one-line wrappers — inline the call instead; extract a custom hook only when the same hook sequence appears in 2+ components.
281
+
282
+ ## State
283
+
284
+ - Local first (`useState`), lift only when shared.
285
+ - Context for cross-cutting, low-frequency reads (theme, auth, i18n) — not for high-frequency updates. Split context by concern: one context per concern so a change to `ThemeContext` doesn't re-render `AuthContext` consumers.
286
+ - External store (Zustand, Jotai, Redux Toolkit) when state must persist across route changes, sync across tabs, or be debugged via devtools.
287
+ - Server-derived data belongs in a server-state library (TanStack Query, SWR, RSC fetch), not application state.
288
+ - Never duplicate state that can be derived — compute during render, never via `useEffect`.
289
+
290
+ ```tsx
291
+ // Bad: derived state stored separately, desyncs, extra render cycle
292
+ const [total, setTotal] = useState(0);
293
+ useEffect(() => {
294
+ setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0));
295
+ }, [items]);
296
+
297
+ // Good: derive during render
298
+ const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
299
+ ```
300
+
301
+ ### State location decision tree
302
+
303
+ 1. Used by one component → `useState` inside it.
304
+ 2. Used by parent + a few children → lift to nearest common ancestor, pass via props.
305
+ 3. Used across distant branches, low-frequency reads → React Context.
306
+ 4. High-frequency updates shared across the tree → external store (Zustand, Jotai, Redux Toolkit).
307
+ 5. Server-derived data → server-state library (TanStack Query, SWR, RSC fetch) — not application state.
308
+
309
+ Context misused for frequently changing values causes every consumer to re-render on every update. Most pages need neither context nor a global store — resist abstraction until duplicated lifting becomes painful.
310
+
311
+ ## Class components
312
+
313
+ Forbidden in new code. Convert legacy class components to function components when touching them for non-trivial changes. Error boundaries remain a class API (no functional equivalent as of React 19) unless using a wrapper library such as `react-error-boundary`.
314
+
315
+ ## File layout per component
316
+
317
+ ```
318
+ components/UserCard/
319
+ UserCard.tsx
320
+ UserCard.module.css # or styled-components, or Tailwind classes inline
321
+ UserCard.test.tsx
322
+ index.ts # re-export only
323
+ ```
324
+
325
+ Inline single-file components are fine for trivial presentational pieces.
326
+
327
+ ## Container / presentational split
328
+
329
+ Container components own data fetching, state, and side effects. Presentational components receive props and render — no service calls, no hooks beyond local UI state.
330
+
331
+ ```tsx
332
+ // Container — owns data
333
+ export function UserPage({ userId }: { userId: string }) {
334
+ const { data: user, isLoading } = useUser(userId);
335
+ if (isLoading) return <Spinner />;
336
+ if (!user) return <NotFound />;
337
+ return <UserCard user={user} onSelect={handleSelect} />;
338
+ }
339
+
340
+ // Presentational — pure
341
+ export function UserCard({ user, onSelect }: { user: User; onSelect: (id: string) => void }) {
342
+ return <button onClick={() => onSelect(user.id)}>{user.name}</button>;
343
+ }
344
+ ```
345
+
346
+ ## Server / Client Component boundary (RSC, Next.js App Router)
347
+
348
+ - Server Components are the default — they run on the server, don't ship to the client, and can `await` directly. Only add `"use client"` when the file uses state, effects, refs, browser APIs, or event handlers; place the directive on line 1, before any imports.
349
+ - Data flows down: a Server Component can render a Client Component and pass serializable props. A Client Component cannot import a Server Component, but it can receive one via `children` or named slots.
350
+ - Never import a Client Component file from inside a `"use server"` action file. Never re-export server-only code through a client module — the bundler will silently include it. Mark sensitive modules with `import "server-only"` so the bundler errors if a client file imports them.
351
+
352
+ ```tsx
353
+ // Server (default)
354
+ export default async function Page() {
355
+ const user = await fetchUser();
356
+ return <UserClient user={user} />;
357
+ }
358
+
359
+ // Client
360
+ "use client";
361
+ export function UserClient({ user }: { user: User }) {
362
+ const [tab, setTab] = useState("profile");
363
+ return <Tabs value={tab} onChange={setTab}>{user.name}</Tabs>;
364
+ }
365
+ ```
366
+
367
+ ## Suspense + error boundaries
368
+
369
+ Every Suspense boundary needs an Error Boundary above it — the pair handles both states. Place boundaries close to where data is needed, not at the route root; multiple narrower boundaries reveal loaded content progressively. A boundary catches errors thrown during render, lifecycle, and constructors of its children — NOT in event handlers or async code.
370
+
371
+ ```tsx
372
+ <ErrorBoundary fallback={<ErrorView />}>
373
+ <Suspense fallback={<Skeleton />}>
374
+ <UserDetails id={id} />
375
+ </Suspense>
376
+ </ErrorBoundary>
377
+ ```
378
+
379
+ ## Forms
380
+
381
+ **Uncontrolled (React 19 + form actions)** — prefer when the form has a clear submit step; the browser owns the value, React reads it via `FormData` on submit.
382
+
383
+ ```tsx
384
+ async function action(formData: FormData) {
385
+ "use server";
386
+ await saveUser({ name: String(formData.get("name")) });
387
+ }
388
+
389
+ export function UserForm() {
390
+ return (
391
+ <form action={action}>
392
+ <input name="name" required />
393
+ <button type="submit">Save</button>
394
+ </form>
395
+ );
396
+ }
397
+ ```
398
+
399
+ **Controlled** — use when the value drives other UI, requires real-time validation, or formatting.
400
+
401
+ **Complex forms** (multi-step, dynamic field arrays, cross-field validation) — use a library: React Hook Form (minimal re-renders, uncontrolled-first), TanStack Form (typed, framework-agnostic), or Final Form. Roll-your-own state management for forms past trivial complexity is a maintenance trap.
402
+
403
+ ## Data fetching
404
+
405
+ | Strategy | When |
406
+ |---|---|
407
+ | RSC fetch (`await` in Server Component) | Per-request data in Next.js App Router, no client-side cache needed |
408
+ | TanStack Query | Client-side cache, mutations, optimistic updates, polling |
409
+ | SWR | Lightweight cache + revalidation, simpler than TanStack Query |
410
+ | `fetch` in `useEffect` | Avoid — race conditions, no cache, no retry. Only acceptable for one-off fire-and-forget |
411
+
412
+ Never fetch in a `useEffect` when a real cache library is available — they handle deduping, cache invalidation, error retry, and Suspense integration.
413
+
414
+ ## Lists and keys
415
+
416
+ `key` must be stable across renders — never `index` for any list that can reorder, insert, or delete. `key` must be unique among siblings, not globally. A reordered list with index keys causes state in child components to attach to the wrong row.
417
+
418
+ ## Composition over inheritance
419
+
420
+ Pass `children` for slot-style composition, render-prop functions for parameterized rendering, component types for plug-in points (`renderItem={UserRow}`). Never extend a component class to specialize behavior.
421
+
422
+ ## Compound components
423
+
424
+ For related controls (Tabs, Accordion, Menu), share state via Context:
425
+
426
+ ```tsx
427
+ <Tabs defaultValue="profile">
428
+ <Tabs.List>
429
+ <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
430
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
431
+ </Tabs.List>
432
+ <Tabs.Panel value="profile"><ProfileForm /></Tabs.Panel>
433
+ <Tabs.Panel value="settings"><SettingsForm /></Tabs.Panel>
434
+ </Tabs>
435
+ ```
436
+
437
+ ## Portals
438
+
439
+ Use `createPortal` for modals, tooltips, toast containers — anything that must escape the parent's `overflow: hidden` or `z-index` stacking context. Render to a stable DOM node mounted in `index.html`.
440
+
441
+ ## Refs (React 19+)
442
+
443
+ React 19 lets function components accept `ref` as a regular prop — `forwardRef` is no longer required for new code. Older codebases on React 18 still need `forwardRef`.
444
+
445
+ ```tsx
446
+ export function Input({ ref, ...rest }: { ref?: React.Ref<HTMLInputElement> } & InputProps) {
447
+ return <input ref={ref} {...rest} />;
448
+ }
449
+ ```
450
+
451
+ ## Accessibility-first composition
452
+
453
+ - Always render semantic HTML (`<button>`, `<a>`, `<nav>`, `<main>`) before reaching for `role` attributes.
454
+ - Every interactive element must be reachable by keyboard.
455
+ - Form inputs need labels — `<label htmlFor>` or `aria-label` if visually labeled by an icon.
456
+ - Manage focus on route changes and modal open/close.
457
+
458
+ ## Out of scope (pointer, not a hard boundary)
459
+
460
+ Next.js App Router specifics (Server Actions, Route Handlers, Middleware, Parallel/Intercepted Routes, streaming Metadata) and React Native (platform-specific imports, `StyleSheet`, navigation libraries) are separate framework concerns — follow their official docs. React core hooks/patterns in this document still apply there.
461
+
462
+ ---
463
+
464
+ # React security
465
+
466
+ ## XSS via `dangerouslySetInnerHTML`
467
+
468
+ CRITICAL. The prop name is deliberately scary — treat every usage as a code review halt.
469
+
470
+ ```tsx
471
+ // CRITICAL: unsanitized user input
472
+ <div dangerouslySetInnerHTML={{ __html: userBio }} />
473
+
474
+ // CORRECT options:
475
+ // 1. Render as text
476
+ <div>{userBio}</div>
477
+
478
+ // 2. Render parsed markdown via a library that sanitizes
479
+ <ReactMarkdown>{userBio}</ReactMarkdown>
480
+
481
+ // 3. If raw HTML is required, sanitize first with DOMPurify
482
+ import DOMPurify from "isomorphic-dompurify";
483
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />
484
+ ```
485
+
486
+ Audit checklist for every `dangerouslySetInnerHTML` call: is the input always under our control (document the source)? If user-derived, is it sanitized at the *same call site* (sanitization only at the API boundary is acceptable if every consumer is verified)? Is the sanitizer config allowlisting tags, not denylisting?
487
+
488
+ ## Unsafe URL schemes
489
+
490
+ `javascript:` and `data:` URLs in `href`, `src`, and `xlink:href` execute arbitrary code. React warns about `javascript:` URLs in `href` in development mode but does not block them at runtime; `data:` URLs and other schemes also slip through — always validate.
491
+
492
+ ```tsx
493
+ // CRITICAL: javascript: URL injection
494
+ <a href={user.website}>Visit</a> // if user.website = "javascript:alert(1)"
495
+
496
+ // CORRECT: validate scheme
497
+ function safeUrl(url: string): string | undefined {
498
+ try {
499
+ const parsed = new URL(url);
500
+ if (["http:", "https:", "mailto:"].includes(parsed.protocol)) return url;
501
+ } catch {
502
+ return undefined;
503
+ }
504
+ return undefined;
505
+ }
506
+ <a href={safeUrl(user.website)}>Visit</a>
507
+ ```
508
+
509
+ ## `target="_blank"` without `rel`
510
+
511
+ `<a target="_blank">` without `rel="noopener noreferrer"` lets the target page access `window.opener` and run navigation hijacks. Modern browsers default to `noopener`, but don't rely on the default — be explicit.
512
+
513
+ ```tsx
514
+ // WRONG
515
+ <a href={externalUrl} target="_blank">External</a>
516
+
517
+ // CORRECT
518
+ <a href={externalUrl} target="_blank" rel="noopener noreferrer">External</a>
519
+ ```
520
+
521
+ ## Server Action input validation
522
+
523
+ Server Actions (`"use server"`) run with the same trust level as a public API endpoint — validate every input, authenticate inside the action (don't trust the client-side route gate), and authorize per-record.
524
+
525
+ ```tsx
526
+ "use server";
527
+ import { z } from "zod";
528
+
529
+ const Input = z.object({
530
+ email: z.string().email(),
531
+ age: z.number().int().min(0).max(120),
532
+ });
533
+
534
+ export async function updateUser(_state: unknown, formData: FormData) {
535
+ const parsed = Input.safeParse({
536
+ email: formData.get("email"),
537
+ age: Number(formData.get("age")),
538
+ });
539
+ if (!parsed.success) return { error: parsed.error.flatten() };
540
+ // ...
541
+ }
542
+ ```
543
+
544
+ ## Secret exposure via env vars
545
+
546
+ Prefixed env vars are bundled into the client — treat them as public.
547
+
548
+ | Framework | Public prefix | Private |
549
+ |---|---|---|
550
+ | Next.js | `NEXT_PUBLIC_*` | All others |
551
+ | Vite | `VITE_*` | `.env` server-side only |
552
+ | Create React App | `REACT_APP_*`, plus `NODE_ENV`, `PUBLIC_URL` | All others |
553
+ | Remix | `process.env` access in `loader`/`action` only | Same |
554
+
555
+ ```ts
556
+ // CRITICAL: secret leaked to client bundle
557
+ const apiKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY;
558
+ ```
559
+
560
+ Audit on every PR that touches env vars: would this string in the public bundle be a problem?
561
+
562
+ ## Authentication / authorization
563
+
564
+ - Never store sessions in `localStorage` — accessible to any XSS. Use httpOnly secure cookies.
565
+ - Never trust client-set state to gate sensitive UI. Render-gating in JSX prevents display, not access — the API must enforce it.
566
+ - CSRF: cookie-based auth requires CSRF tokens or `SameSite=Strict`/`Lax` cookies; use double-submit cookies or origin verification for form actions when not using framework defaults.
567
+
568
+ ## Content Security Policy
569
+
570
+ Configure server-side. Minimum acceptable CSP for a React app:
571
+
572
+ ```
573
+ default-src 'self';
574
+ script-src 'self' 'nonce-{REQUEST_NONCE}';
575
+ style-src 'self' 'unsafe-inline';
576
+ img-src 'self' data: https:;
577
+ connect-src 'self' https://api.example.com;
578
+ frame-ancestors 'none';
579
+ ```
580
+
581
+ Avoid `unsafe-inline`/`unsafe-eval` in `script-src`. For SSR with inline scripts (Next.js streaming, hydration data), use per-request nonces. `style-src 'unsafe-inline'` is often unavoidable for CSS-in-JS — document the tradeoff.
582
+
583
+ ## Prototype pollution via object spread
584
+
585
+ ```tsx
586
+ // WRONG: untrusted JSON spread directly into state
587
+ const update = await req.json();
588
+ setState({ ...state, ...update }); // attacker controls __proto__
589
+
590
+ // CORRECT: parse with a schema, or guard keys
591
+ const Allowed = z.object({ name: z.string(), email: z.string().email() });
592
+ const parsed = Allowed.parse(await req.json());
593
+ setState({ ...state, ...parsed });
594
+ ```
595
+
596
+ ## SSR template injection
597
+
598
+ Values rendered inside JSX are escaped by React — safe. Values passed to `dangerouslySetInnerHTML` are NOT escaped — same rules as client. Manually constructed HTML wrappers around React output must be escaped or sanitized — never concatenate user input into the surrounding HTML template.
599
+
600
+ ## Third-party components
601
+
602
+ Audit `npm audit` before adding any UI library. Check the library doesn't internally use `dangerouslySetInnerHTML` on its input (e.g., rich text editors). Pin versions, review changelogs before major upgrades. Be wary of components that accept raw HTML strings as props.
603
+
604
+ ## Source map exposure in production
605
+
606
+ Production builds should ship without source maps, or with sourcemaps uploaded to an error tracker (Sentry) and stripped from the public bundle — public source maps leak internal logic and file structure.
607
+
608
+ ---
609
+
610
+ # React testing
611
+
612
+ ## Library choice
613
+
614
+ - **React Testing Library (RTL)** — the standard for component testing; tests behavior through the rendered DOM.
615
+ - **Vitest** — preferred runner for new Vite-based projects; faster than Jest, native ESM, same API.
616
+ - **Jest** — still the default for Next.js/CRA projects; RTL works identically.
617
+ - **Playwright Component Testing** — when component tests need a real browser engine (animation, layout, complex events).
618
+ - **Cypress Component Testing** — alternative real-browser component runner.
619
+
620
+ Pick one component test runner per project — do not mix RTL + Playwright CT in the same repo.
621
+
622
+ ## Core principle
623
+
624
+ Test what the user sees and does, not implementation details. Query by accessible role first, then label, then text — fall back to `data-testid` only when nothing else fits. Never assert on internal state, props passed to children, or which hooks were called. Refactor without breaking tests = the test was testing behavior; that's the goal.
625
+
626
+ ## Query priority
627
+
628
+ 1. **Accessible to everyone:** `getByRole(role, { name })` (primary choice), `getByLabelText` (form inputs), `getByPlaceholderText` (when no label — and add one), `getByText` (non-interactive text), `getByDisplayValue` (form field with a current value).
629
+ 2. **Semantic:** `getByAltText` (images), `getByTitle` (last resort, low accessibility value).
630
+ 3. **Test IDs:** `getByTestId("some-id")` — escape hatch only, when none of the above work.
631
+
632
+ `getBy*` throws when no match. `queryBy*` returns `null` (use for asserting absence). `findBy*` returns a promise (use for async).
633
+
634
+ ## User interaction with `userEvent`
635
+
636
+ Prefer `userEvent` over `fireEvent` — it simulates a real browser sequence (focus, keydown, beforeinput, input, keyup); `fireEvent` dispatches a single synthetic event.
637
+
638
+ ```tsx
639
+ import userEvent from "@testing-library/user-event";
640
+
641
+ test("submits the form", async () => {
642
+ const user = userEvent.setup();
643
+ render(<UserForm onSubmit={handleSubmit} />);
644
+
645
+ await user.type(screen.getByLabelText("Email"), "user@example.com");
646
+ await user.click(screen.getByRole("button", { name: /save/i }));
647
+
648
+ expect(handleSubmit).toHaveBeenCalledWith({ email: "user@example.com" });
649
+ });
650
+ ```
651
+
652
+ Always `await` `userEvent` calls — they are async. Call `userEvent.setup()` once at the top of each test, then reuse the returned `user`.
653
+
654
+ ## Async assertions
655
+
656
+ ```tsx
657
+ // WRONG: synchronous query for async-rendered content
658
+ expect(screen.getByText("Loaded")).toBeInTheDocument(); // throws — not in DOM yet
659
+
660
+ // CORRECT: findBy* (returns a promise, retries)
661
+ expect(await screen.findByText("Loaded")).toBeInTheDocument();
662
+
663
+ // CORRECT: waitFor for non-element assertions
664
+ await waitFor(() => expect(saveSpy).toHaveBeenCalled());
665
+
666
+ // CORRECT: element that should disappear
667
+ await waitForElementToBeRemoved(() => screen.queryByText("Loading"));
668
+ ```
669
+
670
+ Never `setTimeout` + assertion — flaky.
671
+
672
+ ## Network mocking with MSW
673
+
674
+ Mock Service Worker mocks at the network layer, so the component, hooks, and fetch library all behave as in production.
675
+
676
+ ```tsx
677
+ // test/setup.ts
678
+ import { setupServer } from "msw/node";
679
+ import { http, HttpResponse } from "msw";
680
+
681
+ export const handlers = [
682
+ http.get("/api/users/:id", ({ params }) =>
683
+ HttpResponse.json({ id: params.id, name: "Alice" }),
684
+ ),
685
+ ];
686
+
687
+ export const server = setupServer(...handlers);
688
+
689
+ beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
690
+ afterEach(() => server.resetHandlers());
691
+ afterAll(() => server.close());
692
+ ```
693
+
694
+ `onUnhandledRequest: "error"` makes any unmocked request fail loudly — silent passes are worse than red. Per-test override:
695
+
696
+ ```tsx
697
+ test("renders error on 500", async () => {
698
+ server.use(http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })));
699
+ render(<UserPage id="1" />);
700
+ expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
701
+ });
702
+ ```
703
+
704
+ ## Provider wrapping
705
+
706
+ Wrap providers once in a `test-utils.tsx` and export from there — use everywhere instead of re-wrapping per test.
707
+
708
+ ```tsx
709
+ function renderWithProviders(ui: React.ReactElement) {
710
+ return render(
711
+ <QueryClientProvider client={new QueryClient()}>
712
+ <ThemeProvider theme={lightTheme}>
713
+ <Router>{ui}</Router>
714
+ </ThemeProvider>
715
+ </QueryClientProvider>,
716
+ );
717
+ }
718
+ ```
719
+
720
+ ## Custom hook testing
721
+
722
+ Use `renderHook`/`act` from RTL. Always wrap state-changing calls in `act`; always test through the public hook API, not internal implementation. For hooks reading context, pass a `wrapper`. Instantiate a shared `QueryClient` once per test, outside the wrapper closure — creating it inside resets cache state on every render and produces flaky tests.
723
+
724
+ ```tsx
725
+ import { renderHook, act } from "@testing-library/react";
726
+
727
+ test("useCounter increments", () => {
728
+ const { result } = renderHook(() => useCounter());
729
+ act(() => result.current.increment());
730
+ expect(result.current.count).toBe(1);
731
+ });
732
+ ```
733
+
734
+ ## Accessibility assertions
735
+
736
+ ```tsx
737
+ import { axe } from "vitest-axe"; // or jest-axe
738
+
739
+ test("UserCard has no a11y violations", async () => {
740
+ const { container } = render(<UserCard user={mockUser} />);
741
+ expect(await axe(container)).toHaveNoViolations();
742
+ });
743
+ ```
744
+
745
+ Run axe assertions in component tests for every interactive component — catches missing labels, invalid ARIA usage, poor color contrast (limited to inline styles; real visual contrast belongs in Playwright), missing alt text, and heading order violations.
746
+
747
+ ## Avoid snapshot tests for components
748
+
749
+ Snapshots of rendered output are brittle, hard to review, and get rubber-stamped by reviewers — they test DOM structure, not behavior. Acceptable uses: pure data serialization functions (stable string output) and generated config files. For component visual regression, use Playwright/Cypress/Percy screenshots — actual visual diffs, not DOM diffs.
750
+
751
+ ## Coverage targets
752
+
753
+ | Layer | Target |
754
+ |---|---|
755
+ | Pure utility functions | ≥90% |
756
+ | Custom hooks | ≥85% |
757
+ | Components (presentational) | ≥80% — behavior, not lines |
758
+ | Container components | ≥70% — golden paths + error states |
759
+ | Pages (E2E covered separately) | Smoke test per route minimum |
760
+
761
+ ## Anti-patterns
762
+
763
+ - Asserting on `container.querySelector` — bypasses accessibility queries.
764
+ - Asserting on number of renders — implementation detail.
765
+ - Mocking React itself or framework hooks (`jest.mock("react", ...)`) — refactor the component instead.
766
+ - Mocking child components by default — tests the integration, not the parent in isolation; mock only when the child has heavy side effects.
767
+ - Ignoring `act()` warnings — they indicate real bugs (state update after unmount, missing async wrapping).
768
+ - Sharing mutable state across tests — flakes when test order changes.
769
+
770
+ ## When to reach for Playwright/Cypress
771
+
772
+ RTL + JSDOM cannot test real layout (flexbox, grid, viewport-dependent rendering), scrolling, drag-and-drop, paste from clipboard, native browser animation/CSS transitions, or cross-frame interactions (iframes, popups). For those, use Playwright Component Testing or full end-to-end Playwright/Cypress runs.
773
+
774
+ ---
775
+
776
+ # React performance
777
+
778
+ Organized by priority — highest-impact categories first. Rules are adapted from Vercel Labs' `react-best-practices` skill (MIT).
779
+
780
+ ## 1. Eliminating waterfalls (CRITICAL)
781
+
782
+ Every sequential `await` adds full network latency — this is the #1 performance killer.
783
+
784
+ - **Cheap conditions before await:** check sync conditions (props, env, hardcoded flags) before awaiting remote data.
785
+ - **Defer awaits until used:** move `await` into the branch that actually needs it, don't await unconditionally up front.
786
+ - **`Promise.all` for independent work:**
787
+
788
+ ```ts
789
+ // INCORRECT — sequential
790
+ const user = await getUser(id);
791
+ const posts = await getPosts(id);
792
+ const followers = await getFollowers(id);
793
+
794
+ // CORRECT — parallel
795
+ const [user, posts, followers] = await Promise.all([
796
+ getUser(id),
797
+ getPosts(id),
798
+ getFollowers(id),
799
+ ]);
800
+ ```
801
+
802
+ - **Partial dependencies — start early, await late:** kick off all promises immediately, only `await` each when its result is actually needed.
803
+ - **Suspense for streaming:** push `<Suspense>` boundaries close to the data so the page paints what it can while slower sub-trees stream in; reserve space (skeleton/`min-height`) for the resulting layout shift.
804
+ - **Server Components: parallel through composition** — sibling `await`s inside one async Server Component run sequentially; split into sibling child components instead so React can run them in parallel.
805
+
806
+ ## 2. Bundle size (CRITICAL)
807
+
808
+ - **Direct imports, not barrels:** `import { Button } from "@/components/Button"` — barrel `index.ts` files force the bundler to walk the entire module graph even when tree-shaking removes most of it; this can cost 200-800ms of first-load JS. Next.js 13.5+'s Optimize Package Imports automates this for listed packages.
809
+ - **Statically analyzable paths:** `name === "home" ? await import("./pages/home") : await import("./pages/about")`, never a template-literal dynamic import path — it defeats bundler/trace analysis.
810
+ - **Dynamic imports for heavy components:** `dynamic(() => import("./HeavyChart"), { loading: () => <Skeleton />, ssr: false })`.
811
+ - **Defer third-party scripts:** load analytics, logging, support widgets after hydration (`next/script` `strategy="afterInteractive"` or `"lazyOnload"`).
812
+ - **Conditional module loading:** `if (user.role === "admin") { const { AdminPanel } = await import("./admin/AdminPanel"); }`.
813
+ - **Preload on hover/focus:** trigger `<link rel="preload">` or `import()` on hover so the bundle is cached by click time.
814
+
815
+ ## 3. Server-side performance (HIGH)
816
+
817
+ - **Authenticate Server Actions like API routes:** every `"use server"` function is a public endpoint — authenticate and authorize inside the action itself, never rely on the calling Client Component's gating.
818
+ - **`React.cache()` for per-request dedup:** `getUser("1")` called from three Server Components in the same render becomes one DB query.
819
+ - **LRU cache for cross-request data** that doesn't change per request (config, lookup tables) — cache outside React with an LRU cache or `unstable_cache`.
820
+ - **Avoid duplicate serialization in RSC props:** when a Server Component renders the same data into multiple Client Components, lift the Client Component up and pass `children` instead.
821
+ - **Hoist static I/O to module scope** — e.g. `readFileSync` for a font at module load, not per-request.
822
+ - **No mutable module-level state in RSC/SSR** — it's shared across all requests, a race condition between users. Use request-scoped storage instead.
823
+ - **Minimize data passed to Client Components** — only serialize what the client needs; strip fields, paginate, project columns at the DB layer.
824
+ - **Parallelize nested fetches** with `Promise.all` per item when enriching a list.
825
+ - **Use `after()`** (Next.js 15) for non-blocking work after the response is sent — logging, cache warming, analytics.
826
+
827
+ ## 4. Client-side data fetching (MEDIUM-HIGH)
828
+
829
+ - Use SWR or TanStack Query for deduplication — multiple components calling `useUser(id)` should share one network request and one cache entry; never roll your own `useEffect` + `fetch` for shared data.
830
+ - Deduplicate global event listeners — one shared listener via a hook + global subject, not one `addEventListener` per component instance.
831
+ - Use passive listeners for scroll (`{ passive: true }`) — improves scroll smoothness (the listener can't `preventDefault()`).
832
+ - `localStorage`: always store a `version` field and bump it on schema change; keep payloads small since the API is synchronous and blocks the main thread.
833
+
834
+ ## 5. Re-render optimization (MEDIUM)
835
+
836
+ - Don't subscribe to state that's only used in callbacks — read it once via `store.getState()` at call time instead of subscribing.
837
+ - Extract expensive work into `memo`-wrapped components so a child re-renders only when its actual props change.
838
+ - Hoist default non-primitive props to a module-level constant (`const EMPTY: Item[] = []`) — an inline `items ?? []` creates a new array every render and breaks memoization.
839
+ - Use primitive dependencies in effects, not object/array literals that get a new identity every render.
840
+ - Subscribe to derived booleans from a store, not the raw collection (`useStore(s => s.cart.length > 0)` re-renders only when emptiness flips).
841
+ - Derive during render, never via `useEffect` — `useEffect(() => setFull(`${first} ${last}`), [first, last])` adds a render cycle for no reason; `const full = `${first} ${last}`` doesn't.
842
+ - Use the functional `setState` updater for stable callbacks (`useCallback(() => setCount(c => c + 1), [])`).
843
+ - Use a lazy state initializer (`useState(() => parseTree(largeInput))`) for expensive initial values.
844
+ - Avoid `useMemo` for cheap primitives (`x + 1`) — memo earns its keep on object identity and genuinely expensive computation.
845
+ - Split hooks with independent dependencies instead of one hook reading multiple unrelated sources, so an unrelated source's update doesn't re-run both selectors.
846
+ - Move interaction logic into event handlers, which run only on the user action, instead of `useEffect`, which re-runs whenever its deps change.
847
+ - Use `startTransition`/`useTransition` for non-urgent updates and `useDeferredValue` for expensive renders driven by fast-changing input.
848
+ - Use `useRef` for transient, frequently-changing values that should not trigger a re-render (timestamps, last-key, accumulators).
849
+ - Don't define components inside components — each render creates a new component type, defeating reconciliation and unmounting children.
850
+
851
+ ## 6. Rendering performance (MEDIUM)
852
+
853
+ - Animate the wrapper `<div>` around an SVG (GPU-accelerated), not the SVG itself (triggers paint).
854
+ - `content-visibility: auto` on long-list rows lets the browser skip offscreen rendering — a major win for lists with hundreds of rows.
855
+ - Hoist static JSX to a module-level constant when it never changes across renders.
856
+ - Reduce SVG coordinate precision (`M10.123456,20.654321` → `M10.12,20.65`) — each digit costs bytes with no visible difference.
857
+ - For hydration no-flicker on values needed before hydration (theme, locale), inline a `<script>` that sets `document.documentElement.dataset.*` before React mounts.
858
+ - Use `suppressHydrationWarning` only on known-divergent leaf nodes (e.g. a `<time>` rendering `Date.now()`), never on a tree containing other children.
859
+ - Use React 19's `<Activity mode="visible|hidden">` for show/hide instead of mount/unmount — cheaper for tabs and accordions since it keeps tree state and effects mounted.
860
+ - Use a ternary, not `&&`, for conditional render when the left side can be falsy-but-not-boolean (`0` renders as a text node).
861
+ - Use `preload`/`preconnect` from `react-dom` for resource hints; `defer`/`async` on `<script>` tags as appropriate.
862
+
863
+ ## 7. JavaScript performance (LOW-MEDIUM)
864
+
865
+ Batch DOM/CSS changes via class swap, not property-by-property. Use `Map` for repeated lookups (`O(1)` vs `O(n)`). Cache property access in loops (`const len = arr.length`). Memoize pure functions with a module-level `Map`. Cache `localStorage` reads — one read per render, not per access. Combine `filter().map()` into a single pass. Check array length before expensive comparisons. Prefer early return. Hoist `RegExp` construction out of loops. Loop for min/max instead of `sort()` (`O(n)` vs `O(n log n)`). Use `Set`/`Map` for membership tests over `Array.includes`. Use `toSorted()` over in-place `.sort()` when immutability matters. Use `flatMap` to map and filter in one pass. Use `requestIdleCallback` for non-critical work.
866
+
867
+ ## 8. Advanced patterns (LOW)
868
+
869
+ - Values from `useEffectEvent` are stable — do not add them to an effect's dependency array.
870
+ - For a stable callback passed to a memoized child, keep the latest handler in a ref and wrap a stable `useCallback` around reading `ref.current`.
871
+ - Guard module-level singleton initialization (telemetry, logger) with a module-scope flag, not `useEffect`.
872
+
873
+ ## Automation note
874
+
875
+ Many of the re-render rules above are now automatable — Next.js 13.5+'s Optimize Package Imports, the React Compiler (auto-memoization), Turbopack, and `@next/bundle-analyzer`. Once a project ships the React Compiler, demote manual `useMemo`/`useCallback` memoization to review-only — it becomes unnecessary noise.
876
+
877
+ ## Web Vitals mapping
878
+
879
+ | Metric | Most relevant categories |
880
+ |---|---|
881
+ | LCP (Largest Contentful Paint) | Waterfalls, Bundle Size, Resource Hints |
882
+ | INP (Interaction to Next Paint) | Re-render, Rendering, JavaScript |
883
+ | CLS (Cumulative Layout Shift) | Rendering (Suspense placement, image dimensions) |
884
+ | TBT (Total Blocking Time) | Bundle Size, JavaScript, Defer Third-Party |
885
+
886
+ ---
887
+
888
+ ## Flag in review (a linter won't catch these)
28
889
 
29
890
  - An exported function with no parameter/return types.
30
891
  - `any` where `unknown` plus a type guard would do.
31
- - In-place mutation of a function argument.
892
+ - In-place mutation of a function argument or of React state.
32
893
  - A repeated inline object shape that should be a named type.
894
+ - A hook called conditionally, or state mutated directly then passed to `setState`.
895
+ - `dangerouslySetInnerHTML` with unsanitized input, or an unvalidated URL scheme in `href`/`src`.
896
+ - `target="_blank"` without `rel="noopener noreferrer"`.
897
+ - A `"use server"` action with no schema validation or authorization check.
898
+ - A `NEXT_PUBLIC_*`/`VITE_*`/`REACT_APP_*` env var holding a private secret.
899
+ - `key={index}` on a list that can reorder, insert, or delete.
900
+ - Sequential `await`s that could run via `Promise.all`.
33
901
 
34
902
  ## Workflow pack: TypeScript rules
35
903
 
36
- TypeScript conventions a linter can't check explicit public-API types, unknown over any with narrowing, immutable updates, Zod at the boundary — with a reviewer and a prettier guard; formatting and lint rules belong in CI.
904
+ Full TypeScript + React standards (types, immutability, errors, RSC boundaries, hooks, security, testing, performance) with three reviewers, four skills, and a guarded typecheck hook the complete ECC TS/React corpus, not a distillation.
37
905
 
38
906
  ### Roles
39
907
 
40
908
  - **typescript-reviewer** — Reviews changed TypeScript/JavaScript against the standards: explicit public-API types, unknown over any, immutable updates, Zod boundaries, and env-var secrets. Reports file:line + severity. Use after any TS/JS change.
909
+ - **react-reviewer** — Expert React/JSX code reviewer specializing in hook correctness, render performance, server/client component boundaries, accessibility, and React-specific security. Use for any change touching .tsx/.jsx files or React component logic.
910
+ - **type-design-analyzer** — Analyzes type design for encapsulation, invariant expression, usefulness, and enforcement — whether types make illegal states harder or impossible to represent. Use when reviewing a new or changed type/interface for domain modeling quality, not just correctness.
41
911
 
42
912
  ### Commands
43
913
 
44
914
  - **/typescript-review** `[base ref]` — Run the TypeScript reviewer over the current change against this pack's TypeScript standards.
45
915
  - How it runs: Invoke the typescript-reviewer subagent on the changed TypeScript files (the diff since $ARGUMENTS, or the working tree if no ref is given). It checks this pack's conventions — explicit public-API types, unknown over any with narrowing, immutable updates, and Zod at the boundary — and reports file:line + severity. Formatting and lint rules are CI's job; don't re-litigate them. Fix any Critical or Important findings before merging.
916
+ - **/react-review** `[base ref]` — Run the React reviewer over the current change against this pack's React standards.
917
+ - How it runs: Invoke the react-reviewer subagent on the changed .tsx/.jsx files (the diff since $ARGUMENTS, or the working tree if no ref is given). It checks this pack's React conventions — hook correctness, render performance, server/client boundaries, accessibility, and React-specific security (dangerouslySetInnerHTML, unsafe URL schemes, Server Action validation, env var leaks) — and reports file:line + severity. For a JSX/TSX change, also run /typescript-review — the two reviewers cover disjoint lanes. Fix any Critical or Important findings before merging.
46
918
 
47
919
  ### Guardrails
48
920
 
49
921
  - After an edit, check formatting with prettier when it is installed.
922
+ - After an edit, typecheck with tsc when the project has a tsconfig.json.
50
923
 
51
924
  ### Skills
52
925
 
53
926
  - **immutable-updates** — Use when updating objects, arrays, or records in application code — return a new copy with the change applied instead of mutating the original in place, to prevent hidden side effects and aliasing bugs.
927
+ - **react-patterns** — 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.
928
+ - **react-performance** — 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.
929
+ - **react-testing** — 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.
54
930
 
55
931
  Tool-specific implementations live alongside this file (see `CLAUDE.md` for the Claude Code implementation). Tools without a native subagent/command surface should treat the sections above as operating instructions.