@baselane/packs 0.1.3 → 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.
- package/dist/aliases.js +1 -0
- package/package.json +2 -2
- package/packs/database-review/pack.json +75 -9
- package/packs/database-review/rendered/.claude/agents/database-reviewer.md +75 -2
- package/packs/database-review/rendered/.claude/commands/migration-review.md +6 -0
- package/packs/database-review/rendered/.claude/settings.json +11 -0
- package/packs/database-review/rendered/.claude/skills/database-migrations/SKILL.md +431 -0
- package/packs/database-review/rendered/.claude/skills/mysql-patterns/SKILL.md +414 -0
- package/packs/database-review/rendered/.claude/skills/postgres-patterns/SKILL.md +149 -0
- package/packs/database-review/rendered/.github/copilot-instructions.md +1096 -14
- package/packs/database-review/rendered/AGENTS.md +45 -16
- package/packs/database-review/rendered/CLAUDE.md +4 -0
- package/packs/database-review/rendered/GEMINI.md +987 -0
- package/packs/go-rules/pack.json +65 -9
- package/packs/go-rules/rendered/.claude/agents/go-build-resolver.md +90 -0
- package/packs/go-rules/rendered/.claude/agents/go-reviewer.md +72 -2
- package/packs/go-rules/rendered/.claude/commands/go-build.md +6 -0
- package/packs/go-rules/rendered/.claude/commands/go-review.md +1 -1
- package/packs/go-rules/rendered/.claude/commands/go-test.md +6 -0
- package/packs/go-rules/rendered/.claude/settings.json +9 -0
- package/packs/go-rules/rendered/.claude/skills/golang-patterns/SKILL.md +673 -0
- package/packs/go-rules/rendered/.claude/skills/golang-testing/SKILL.md +719 -0
- package/packs/go-rules/rendered/.github/copilot-instructions.md +1783 -7
- package/packs/go-rules/rendered/AGENTS.md +244 -8
- package/packs/go-rules/rendered/CLAUDE.md +5 -0
- package/packs/go-rules/rendered/GEMINI.md +1394 -0
- package/packs/python-rules/pack.json +87 -10
- package/packs/python-rules/rendered/.claude/agents/django-reviewer.md +159 -0
- package/packs/python-rules/rendered/.claude/agents/fastapi-reviewer.md +70 -0
- package/packs/python-rules/rendered/.claude/agents/python-reviewer.md +92 -2
- package/packs/python-rules/rendered/.claude/commands/django-review.md +6 -0
- package/packs/python-rules/rendered/.claude/commands/fastapi-review.md +6 -0
- package/packs/python-rules/rendered/.claude/commands/python-review.md +1 -1
- package/packs/python-rules/rendered/.claude/settings.json +1 -1
- package/packs/python-rules/rendered/.claude/skills/django-security/SKILL.md +642 -0
- package/packs/python-rules/rendered/.claude/skills/django-tdd/SKILL.md +728 -0
- package/packs/python-rules/rendered/.claude/skills/fastapi-patterns/SKILL.md +512 -0
- package/packs/python-rules/rendered/.claude/skills/python-patterns/SKILL.md +749 -0
- package/packs/python-rules/rendered/.claude/skills/python-testing/SKILL.md +815 -0
- package/packs/python-rules/rendered/.github/copilot-instructions.md +3815 -12
- package/packs/python-rules/rendered/AGENTS.md +73 -13
- package/packs/python-rules/rendered/CLAUDE.md +9 -0
- package/packs/python-rules/rendered/GEMINI.md +3448 -0
- package/packs/security-review/pack.json +51 -7
- package/packs/security-review/rendered/.claude/agents/security-reviewer.md +29 -2
- package/packs/security-review/rendered/.claude/settings.json +11 -0
- package/packs/security-review/rendered/.claude/skills/security-review/SKILL.md +302 -0
- package/packs/security-review/rendered/.claude/skills/security-review/cloud-infrastructure-security.md +171 -0
- package/packs/security-review/rendered/.claude/skills/security-scan-config/SKILL.md +56 -0
- package/packs/security-review/rendered/.github/copilot-instructions.md +658 -15
- package/packs/security-review/rendered/AGENTS.md +283 -17
- package/packs/security-review/rendered/CLAUDE.md +2 -0
- package/packs/security-review/rendered/GEMINI.md +352 -0
- package/packs/software-engineer-harness/pack.json +249 -13
- package/packs/software-engineer-harness/rendered/.claude/agents/build-error-resolver.md +15 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/code-explorer.md +67 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/performance-optimizer.md +444 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/pr-test-analyzer.md +43 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/refactor-cleaner.md +12 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/silent-failure-hunter.md +16 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/tdd-guide.md +16 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/feature-dev.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/fix-build.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/hunt-silent-failures.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/tdd.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/settings.json +29 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/api-design/SKILL.md +522 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/architecture-decision-records/SKILL.md +178 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/backend-patterns/SKILL.md +560 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/codebase-onboarding/SKILL.md +232 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/deployment-patterns/SKILL.md +426 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/docker-patterns/SKILL.md +363 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/e2e-testing/SKILL.md +325 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/tdd-red-green-refactor/SKILL.md +20 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/tdd-workflow/SKILL.md +580 -0
- package/packs/software-engineer-harness/rendered/.github/copilot-instructions.md +3948 -2
- package/packs/software-engineer-harness/rendered/AGENTS.md +194 -4
- package/packs/software-engineer-harness/rendered/CLAUDE.md +20 -0
- package/packs/software-engineer-harness/rendered/GEMINI.md +3208 -0
- package/packs/typescript-rules/pack.json +68 -5
- package/packs/typescript-rules/rendered/.claude/agents/react-reviewer.md +165 -0
- package/packs/typescript-rules/rendered/.claude/agents/type-design-analyzer.md +48 -0
- package/packs/typescript-rules/rendered/.claude/commands/react-review.md +6 -0
- package/packs/typescript-rules/rendered/.claude/settings.json +9 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-patterns/SKILL.md +340 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-performance/SKILL.md +573 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-testing/SKILL.md +422 -0
- package/packs/typescript-rules/rendered/.github/copilot-instructions.md +2459 -53
- package/packs/typescript-rules/rendered/AGENTS.md +893 -17
- package/packs/typescript-rules/rendered/CLAUDE.md +6 -0
- package/packs/typescript-rules/rendered/GEMINI.md +1335 -0
|
@@ -1,55 +1,931 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
<!-- generated from workflow-pack typescript-rules
|
|
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
|
|
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
|
-
|
|
10
|
+
## Types and interfaces
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
```typescript
|
|
19
|
+
// WRONG: exported function without explicit types
|
|
20
|
+
export function formatUser(user) {
|
|
21
|
+
return `${user.firstName} ${user.lastName}`
|
|
22
|
+
}
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
// CORRECT: explicit types on public APIs
|
|
25
|
+
interface User {
|
|
26
|
+
firstName: string
|
|
27
|
+
lastName: string
|
|
28
|
+
}
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
export function formatUser(user: User): string {
|
|
31
|
+
return `${user.firstName} ${user.lastName}`
|
|
32
|
+
}
|
|
33
|
+
```
|
|
26
34
|
|
|
27
|
-
###
|
|
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
|
|
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.
|