tribunal-kit 2.4.6 → 3.0.0
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/.agent/agents/accessibility-reviewer.md +220 -134
- package/.agent/agents/ai-code-reviewer.md +233 -129
- package/.agent/agents/backend-specialist.md +238 -178
- package/.agent/agents/code-archaeologist.md +181 -119
- package/.agent/agents/database-architect.md +207 -164
- package/.agent/agents/debugger.md +218 -151
- package/.agent/agents/dependency-reviewer.md +136 -55
- package/.agent/agents/devops-engineer.md +238 -175
- package/.agent/agents/documentation-writer.md +221 -137
- package/.agent/agents/explorer-agent.md +180 -142
- package/.agent/agents/frontend-reviewer.md +194 -80
- package/.agent/agents/frontend-specialist.md +237 -188
- package/.agent/agents/game-developer.md +52 -184
- package/.agent/agents/logic-reviewer.md +149 -78
- package/.agent/agents/mobile-developer.md +223 -152
- package/.agent/agents/mobile-reviewer.md +195 -79
- package/.agent/agents/orchestrator.md +211 -170
- package/.agent/agents/penetration-tester.md +174 -131
- package/.agent/agents/performance-optimizer.md +203 -139
- package/.agent/agents/performance-reviewer.md +211 -108
- package/.agent/agents/product-manager.md +162 -108
- package/.agent/agents/project-planner.md +162 -142
- package/.agent/agents/qa-automation-engineer.md +242 -138
- package/.agent/agents/security-auditor.md +194 -170
- package/.agent/agents/seo-specialist.md +213 -132
- package/.agent/agents/sql-reviewer.md +194 -73
- package/.agent/agents/supervisor-agent.md +203 -156
- package/.agent/agents/test-coverage-reviewer.md +193 -81
- package/.agent/agents/type-safety-reviewer.md +208 -65
- package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
- package/.agent/skills/agent-organizer/SKILL.md +126 -132
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +155 -66
- package/.agent/skills/api-patterns/SKILL.md +289 -257
- package/.agent/skills/api-security-auditor/SKILL.md +172 -70
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/appflow-wireframe/SKILL.md +107 -100
- package/.agent/skills/architecture/SKILL.md +331 -200
- package/.agent/skills/authentication-best-practices/SKILL.md +168 -67
- package/.agent/skills/bash-linux/SKILL.md +154 -215
- package/.agent/skills/brainstorming/SKILL.md +104 -210
- package/.agent/skills/building-native-ui/SKILL.md +169 -70
- package/.agent/skills/clean-code/SKILL.md +360 -206
- package/.agent/skills/config-validator/SKILL.md +141 -165
- package/.agent/skills/csharp-developer/SKILL.md +528 -107
- package/.agent/skills/database-design/SKILL.md +455 -275
- package/.agent/skills/deployment-procedures/SKILL.md +145 -188
- package/.agent/skills/devops-engineer/SKILL.md +332 -134
- package/.agent/skills/devops-incident-responder/SKILL.md +113 -98
- package/.agent/skills/edge-computing/SKILL.md +157 -213
- package/.agent/skills/extract-design-system/SKILL.md +129 -69
- package/.agent/skills/framer-motion-expert/SKILL.md +939 -0
- package/.agent/skills/game-design-expert/SKILL.md +105 -0
- package/.agent/skills/game-engineering-expert/SKILL.md +122 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +124 -215
- package/.agent/skills/github-operations/SKILL.md +314 -354
- package/.agent/skills/gsap-expert/SKILL.md +901 -0
- package/.agent/skills/i18n-localization/SKILL.md +138 -216
- package/.agent/skills/intelligent-routing/SKILL.md +127 -139
- package/.agent/skills/llm-engineering/SKILL.md +357 -258
- package/.agent/skills/local-first/SKILL.md +154 -203
- package/.agent/skills/mcp-builder/SKILL.md +118 -224
- package/.agent/skills/nextjs-react-expert/SKILL.md +783 -203
- package/.agent/skills/nodejs-best-practices/SKILL.md +559 -280
- package/.agent/skills/observability/SKILL.md +330 -285
- package/.agent/skills/parallel-agents/SKILL.md +122 -181
- package/.agent/skills/performance-profiling/SKILL.md +254 -197
- package/.agent/skills/plan-writing/SKILL.md +118 -188
- package/.agent/skills/platform-engineer/SKILL.md +123 -135
- package/.agent/skills/playwright-best-practices/SKILL.md +157 -76
- package/.agent/skills/powershell-windows/SKILL.md +146 -230
- package/.agent/skills/python-pro/SKILL.md +879 -114
- package/.agent/skills/react-specialist/SKILL.md +931 -108
- package/.agent/skills/realtime-patterns/SKILL.md +304 -296
- package/.agent/skills/rust-pro/SKILL.md +701 -240
- package/.agent/skills/seo-fundamentals/SKILL.md +154 -181
- package/.agent/skills/server-management/SKILL.md +190 -212
- package/.agent/skills/shadcn-ui-expert/SKILL.md +201 -68
- package/.agent/skills/sql-pro/SKILL.md +633 -104
- package/.agent/skills/swiftui-expert/SKILL.md +171 -70
- package/.agent/skills/systematic-debugging/SKILL.md +118 -186
- package/.agent/skills/tailwind-patterns/SKILL.md +576 -232
- package/.agent/skills/tdd-workflow/SKILL.md +137 -209
- package/.agent/skills/testing-patterns/SKILL.md +573 -205
- package/.agent/skills/vue-expert/SKILL.md +964 -119
- package/.agent/skills/vulnerability-scanner/SKILL.md +269 -316
- package/.agent/skills/web-accessibility-auditor/SKILL.md +188 -71
- package/.agent/skills/webapp-testing/SKILL.md +145 -236
- package/.agent/workflows/api-tester.md +151 -279
- package/.agent/workflows/audit.md +138 -168
- package/.agent/workflows/brainstorm.md +110 -146
- package/.agent/workflows/changelog.md +112 -144
- package/.agent/workflows/create.md +124 -139
- package/.agent/workflows/debug.md +189 -196
- package/.agent/workflows/deploy.md +189 -153
- package/.agent/workflows/enhance.md +151 -139
- package/.agent/workflows/fix.md +135 -143
- package/.agent/workflows/generate.md +157 -164
- package/.agent/workflows/migrate.md +160 -163
- package/.agent/workflows/orchestrate.md +168 -151
- package/.agent/workflows/performance-benchmarker.md +123 -305
- package/.agent/workflows/plan.md +173 -151
- package/.agent/workflows/preview.md +80 -137
- package/.agent/workflows/refactor.md +183 -153
- package/.agent/workflows/review-ai.md +129 -140
- package/.agent/workflows/review.md +116 -155
- package/.agent/workflows/session.md +94 -154
- package/.agent/workflows/status.md +79 -125
- package/.agent/workflows/strengthen-skills.md +139 -99
- package/.agent/workflows/swarm.md +179 -194
- package/.agent/workflows/test.md +211 -166
- package/.agent/workflows/tribunal-backend.md +113 -111
- package/.agent/workflows/tribunal-database.md +115 -132
- package/.agent/workflows/tribunal-frontend.md +118 -115
- package/.agent/workflows/tribunal-full.md +133 -136
- package/.agent/workflows/tribunal-mobile.md +119 -123
- package/.agent/workflows/tribunal-performance.md +133 -152
- package/.agent/workflows/ui-ux-pro-max.md +143 -171
- package/README.md +11 -15
- package/package.json +1 -1
- package/.agent/skills/dotnet-core-expert/SKILL.md +0 -103
- package/.agent/skills/framer-motion-animations/SKILL.md +0 -74
- package/.agent/skills/game-development/2d-games/SKILL.md +0 -119
- package/.agent/skills/game-development/3d-games/SKILL.md +0 -135
- package/.agent/skills/game-development/SKILL.md +0 -236
- package/.agent/skills/game-development/game-art/SKILL.md +0 -185
- package/.agent/skills/game-development/game-audio/SKILL.md +0 -190
- package/.agent/skills/game-development/game-design/SKILL.md +0 -129
- package/.agent/skills/game-development/mobile-games/SKILL.md +0 -108
- package/.agent/skills/game-development/multiplayer/SKILL.md +0 -132
- package/.agent/skills/game-development/pc-games/SKILL.md +0 -144
- package/.agent/skills/game-development/vr-ar/SKILL.md +0 -123
- package/.agent/skills/game-development/web-games/SKILL.md +0 -150
|
@@ -1,108 +1,931 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: react-specialist
|
|
3
|
-
description: Senior React specialist (React
|
|
4
|
-
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
-
version:
|
|
6
|
-
last-updated: 2026-03-
|
|
7
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# React Specialist
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
1
|
+
---
|
|
2
|
+
name: react-specialist
|
|
3
|
+
description: Senior React specialist (React 19+) focusing on advanced patterns, hooks mastery, React Compiler, Server Components, state management (Zustand/Jotai/React Query), performance optimization, and production architectures (Next.js/Remix). Use when building React components, optimizing renders, managing state, or implementing modern React 19 patterns.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
last-updated: 2026-03-30
|
|
7
|
+
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# React Specialist — React 19+ Mastery
|
|
11
|
+
|
|
12
|
+
> React 19 is a paradigm shift. Server Components are the default. The React Compiler handles memoization. `use()` replaces `useEffect` data fetching. If you're still writing React 18 patterns, you're writing legacy code.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## React 19 Core API Changes
|
|
17
|
+
|
|
18
|
+
### The `use()` Hook
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// use() can read promises and context — replaces many useEffect patterns
|
|
22
|
+
import { use } from "react";
|
|
23
|
+
|
|
24
|
+
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
|
|
25
|
+
const user = use(userPromise); // suspends until resolved
|
|
26
|
+
return <h1>{user.name}</h1>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// use() with context (replaces useContext — can be used conditionally)
|
|
30
|
+
function Theme({ isAdmin }: { isAdmin: boolean }) {
|
|
31
|
+
if (isAdmin) {
|
|
32
|
+
const theme = use(ThemeContext); // ✅ conditional context read
|
|
33
|
+
return <AdminPanel theme={theme} />;
|
|
34
|
+
}
|
|
35
|
+
return <PublicPanel />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ❌ HALLUCINATION TRAP: use() is NOT useContext()
|
|
39
|
+
// useContext cannot be called inside conditionals or loops
|
|
40
|
+
// use() CAN be called inside conditionals (it's a new primitive)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `useActionState` (Forms)
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { useActionState } from "react";
|
|
47
|
+
|
|
48
|
+
async function submitForm(prevState: FormState, formData: FormData) {
|
|
49
|
+
const email = formData.get("email") as string;
|
|
50
|
+
|
|
51
|
+
if (!email.includes("@")) {
|
|
52
|
+
return { error: "Invalid email", success: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await saveToDatabase(email);
|
|
56
|
+
return { error: null, success: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SignupForm() {
|
|
60
|
+
const [state, formAction, isPending] = useActionState(submitForm, {
|
|
61
|
+
error: null,
|
|
62
|
+
success: false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<form action={formAction}>
|
|
67
|
+
<input name="email" type="email" disabled={isPending} />
|
|
68
|
+
{state.error && <p className="error">{state.error}</p>}
|
|
69
|
+
<button type="submit" disabled={isPending}>
|
|
70
|
+
{isPending ? "Submitting..." : "Sign Up"}
|
|
71
|
+
</button>
|
|
72
|
+
</form>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ❌ HALLUCINATION TRAP: useActionState was briefly named useFormState
|
|
77
|
+
// in React canaries. The STABLE name is useActionState.
|
|
78
|
+
// ❌ HALLUCINATION TRAP: The signature is (action, initialState)
|
|
79
|
+
// The action receives (prevState, formData), NOT just formData
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `useOptimistic`
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { useOptimistic } from "react";
|
|
86
|
+
|
|
87
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
88
|
+
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
|
|
89
|
+
todos,
|
|
90
|
+
(currentTodos, newTodo: Todo) => [...currentTodos, newTodo]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
async function handleAdd(formData: FormData) {
|
|
94
|
+
const title = formData.get("title") as string;
|
|
95
|
+
const tempTodo = { id: crypto.randomUUID(), title, pending: true };
|
|
96
|
+
|
|
97
|
+
addOptimisticTodo(tempTodo); // instantly updates UI
|
|
98
|
+
|
|
99
|
+
await saveTodo(title); // actual API call
|
|
100
|
+
// When server responds, `todos` prop updates and optimistic state resets
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div>
|
|
105
|
+
<form action={handleAdd}>
|
|
106
|
+
<input name="title" />
|
|
107
|
+
<button type="submit">Add</button>
|
|
108
|
+
</form>
|
|
109
|
+
<ul>
|
|
110
|
+
{optimisticTodos.map((todo) => (
|
|
111
|
+
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
|
|
112
|
+
{todo.title}
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `useFormStatus`
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useFormStatus } from "react-dom";
|
|
125
|
+
|
|
126
|
+
// ❌ HALLUCINATION TRAP: useFormStatus must be called from a component
|
|
127
|
+
// INSIDE a <form> — it reads the nearest parent form's status.
|
|
128
|
+
// It does NOT work if called in the same component that renders the <form>.
|
|
129
|
+
|
|
130
|
+
function SubmitButton() {
|
|
131
|
+
const { pending, data, method, action } = useFormStatus();
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<button type="submit" disabled={pending}>
|
|
135
|
+
{pending ? "Saving..." : "Save"}
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Usage:
|
|
141
|
+
function MyForm() {
|
|
142
|
+
return (
|
|
143
|
+
<form action={serverAction}>
|
|
144
|
+
<input name="name" />
|
|
145
|
+
<SubmitButton /> {/* useFormStatus works here — inside the form */}
|
|
146
|
+
</form>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `useTransition` (Non-Blocking State Updates)
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { useTransition } from "react";
|
|
155
|
+
|
|
156
|
+
function SearchPage() {
|
|
157
|
+
const [query, setQuery] = useState("");
|
|
158
|
+
const [results, setResults] = useState<Item[]>([]);
|
|
159
|
+
const [isPending, startTransition] = useTransition();
|
|
160
|
+
|
|
161
|
+
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
|
|
162
|
+
const value = e.target.value;
|
|
163
|
+
setQuery(value); // urgent — update input immediately
|
|
164
|
+
|
|
165
|
+
startTransition(async () => {
|
|
166
|
+
// non-urgent — React can interrupt this if user types again
|
|
167
|
+
const data = await search(value);
|
|
168
|
+
setResults(data);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div>
|
|
174
|
+
<input value={query} onChange={handleSearch} />
|
|
175
|
+
{isPending && <Spinner />}
|
|
176
|
+
<ResultsList results={results} />
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// React 19: startTransition now supports async functions
|
|
182
|
+
// React 18: startTransition was synchronous only
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `useDeferredValue`
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { useDeferredValue, memo } from "react";
|
|
189
|
+
|
|
190
|
+
function SearchResults({ query }: { query: string }) {
|
|
191
|
+
const deferredQuery = useDeferredValue(query);
|
|
192
|
+
|
|
193
|
+
// Shows stale results while the new ones compute
|
|
194
|
+
const isStale = query !== deferredQuery;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div style={{ opacity: isStale ? 0.6 : 1 }}>
|
|
198
|
+
<ExpensiveList query={deferredQuery} />
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Initial value support (React 19):
|
|
204
|
+
const value = useDeferredValue(fetchedData, initialFallback);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## The React Compiler
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
// React 19 ships the React Compiler (formerly React Forget)
|
|
213
|
+
// It automatically memoizes components, values, and callbacks
|
|
214
|
+
|
|
215
|
+
// ❌ LEGACY — do NOT write this in React 19+
|
|
216
|
+
const memoizedValue = useMemo(() => expensiveCalc(a, b), [a, b]);
|
|
217
|
+
const memoizedFn = useCallback(() => handleClick(id), [id]);
|
|
218
|
+
const MemoizedComp = React.memo(MyComponent);
|
|
219
|
+
|
|
220
|
+
// ✅ REACT 19 — just write normal code
|
|
221
|
+
const value = expensiveCalc(a, b);
|
|
222
|
+
function handleClick() { /* ... */ }
|
|
223
|
+
function MyComponent() { /* ... */ }
|
|
224
|
+
// The compiler figures out what needs memoization automatically
|
|
225
|
+
|
|
226
|
+
// ❌ HALLUCINATION TRAP: Do NOT manually memoize in React 19+ projects
|
|
227
|
+
// The compiler is smarter than manual memoization and handles:
|
|
228
|
+
// - Component memoization (replaces React.memo)
|
|
229
|
+
// - Value memoization (replaces useMemo)
|
|
230
|
+
// - Callback memoization (replaces useCallback)
|
|
231
|
+
//
|
|
232
|
+
// EXCEPTION: If the React Compiler is explicitly disabled in the project
|
|
233
|
+
// config, then manual memoization is still appropriate.
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### When Manual Memoization Is Still Valid
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
// 1. React Compiler is disabled in project config
|
|
240
|
+
// 2. Working with React 18 (no compiler)
|
|
241
|
+
// 3. Library code that must support React 17/18/19
|
|
242
|
+
// 4. Performance-critical code where compiler output is insufficient
|
|
243
|
+
// (measure first with React DevTools Profiler)
|
|
244
|
+
|
|
245
|
+
// Always add a comment explaining why:
|
|
246
|
+
// MANUAL_MEMO: React Compiler disabled in this project
|
|
247
|
+
const cached = useMemo(() => heavyComputation(data), [data]);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Component Architecture Patterns
|
|
253
|
+
|
|
254
|
+
### Compound Components
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
// Compound components share implicit state via context
|
|
258
|
+
const TabsContext = createContext<{
|
|
259
|
+
activeTab: string;
|
|
260
|
+
setActiveTab: (id: string) => void;
|
|
261
|
+
} | null>(null);
|
|
262
|
+
|
|
263
|
+
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
|
|
264
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
265
|
+
return (
|
|
266
|
+
<TabsContext value={{ activeTab, setActiveTab }}>
|
|
267
|
+
<div className="tabs">{children}</div>
|
|
268
|
+
</TabsContext>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function TabList({ children }: { children: ReactNode }) {
|
|
273
|
+
return <div role="tablist" className="tab-list">{children}</div>;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function Tab({ id, children }: { id: string; children: ReactNode }) {
|
|
277
|
+
const ctx = use(TabsContext);
|
|
278
|
+
if (!ctx) throw new Error("Tab must be used inside <Tabs>");
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<button
|
|
282
|
+
role="tab"
|
|
283
|
+
aria-selected={ctx.activeTab === id}
|
|
284
|
+
onClick={() => ctx.setActiveTab(id)}
|
|
285
|
+
>
|
|
286
|
+
{children}
|
|
287
|
+
</button>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function TabPanel({ id, children }: { id: string; children: ReactNode }) {
|
|
292
|
+
const ctx = use(TabsContext);
|
|
293
|
+
if (!ctx) throw new Error("TabPanel must be used inside <Tabs>");
|
|
294
|
+
if (ctx.activeTab !== id) return null;
|
|
295
|
+
|
|
296
|
+
return <div role="tabpanel">{children}</div>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Usage:
|
|
300
|
+
<Tabs defaultTab="settings">
|
|
301
|
+
<TabList>
|
|
302
|
+
<Tab id="profile">Profile</Tab>
|
|
303
|
+
<Tab id="settings">Settings</Tab>
|
|
304
|
+
</TabList>
|
|
305
|
+
<TabPanel id="profile"><ProfileContent /></TabPanel>
|
|
306
|
+
<TabPanel id="settings"><SettingsContent /></TabPanel>
|
|
307
|
+
</Tabs>
|
|
308
|
+
|
|
309
|
+
// ❌ HALLUCINATION TRAP: In React 19, context uses <Ctx value={}>
|
|
310
|
+
// NOT <Ctx.Provider value={}>. The .Provider pattern is deprecated.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Custom Hooks (Composable Logic)
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
// useFetch — reusable data fetching with loading/error states
|
|
317
|
+
function useFetch<T>(url: string) {
|
|
318
|
+
const [data, setData] = useState<T | null>(null);
|
|
319
|
+
const [error, setError] = useState<Error | null>(null);
|
|
320
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
const controller = new AbortController();
|
|
324
|
+
|
|
325
|
+
async function fetchData() {
|
|
326
|
+
try {
|
|
327
|
+
setIsLoading(true);
|
|
328
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
329
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
330
|
+
const json = await res.json();
|
|
331
|
+
setData(json);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
334
|
+
setError(err);
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
setIsLoading(false);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fetchData();
|
|
342
|
+
return () => controller.abort(); // cleanup on unmount or URL change
|
|
343
|
+
}, [url]);
|
|
344
|
+
|
|
345
|
+
return { data, error, isLoading };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ❌ HALLUCINATION TRAP: Always include AbortController cleanup
|
|
349
|
+
// Without it, state updates on unmounted components cause warnings
|
|
350
|
+
// and potential memory leaks in SPAs
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Error Boundaries
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
// React 19 error boundaries — still class-based (no hook equivalent yet)
|
|
357
|
+
class ErrorBoundary extends Component<
|
|
358
|
+
{ children: ReactNode; fallback: ReactNode },
|
|
359
|
+
{ hasError: boolean; error: Error | null }
|
|
360
|
+
> {
|
|
361
|
+
state = { hasError: false, error: null };
|
|
362
|
+
|
|
363
|
+
static getDerivedStateFromError(error: Error) {
|
|
364
|
+
return { hasError: true, error };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
368
|
+
console.error("ErrorBoundary caught:", error, info.componentStack);
|
|
369
|
+
// Send to error tracking service (Sentry, etc.)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
render() {
|
|
373
|
+
if (this.state.hasError) return this.props.fallback;
|
|
374
|
+
return this.props.children;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Usage:
|
|
379
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
380
|
+
<Suspense fallback={<Loading />}>
|
|
381
|
+
<Dashboard />
|
|
382
|
+
</Suspense>
|
|
383
|
+
</ErrorBoundary>
|
|
384
|
+
|
|
385
|
+
// ❌ HALLUCINATION TRAP: There is NO useErrorBoundary hook in React core.
|
|
386
|
+
// Error boundaries MUST be class components.
|
|
387
|
+
// react-error-boundary (npm package) provides a hook-based wrapper.
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Render Props & Slots
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
// Render prop for flexible rendering
|
|
394
|
+
interface DataTableProps<T> {
|
|
395
|
+
data: T[];
|
|
396
|
+
renderRow: (item: T, index: number) => ReactNode;
|
|
397
|
+
renderHeader?: () => ReactNode;
|
|
398
|
+
renderEmpty?: () => ReactNode;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function DataTable<T>({ data, renderRow, renderHeader, renderEmpty }: DataTableProps<T>) {
|
|
402
|
+
if (data.length === 0) return renderEmpty?.() ?? <p>No data</p>;
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<table>
|
|
406
|
+
{renderHeader && <thead>{renderHeader()}</thead>}
|
|
407
|
+
<tbody>
|
|
408
|
+
{data.map((item, i) => (
|
|
409
|
+
<tr key={i}>{renderRow(item, i)}</tr>
|
|
410
|
+
))}
|
|
411
|
+
</tbody>
|
|
412
|
+
</table>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## State Management Decision Matrix
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
423
|
+
│ State Type Decision Tree │
|
|
424
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
425
|
+
│ │
|
|
426
|
+
│ Is it SERVER data (fetched from API/DB)? │
|
|
427
|
+
│ ├── YES → TanStack React Query / SWR │
|
|
428
|
+
│ │ (caching, deduplication, revalidation, optimistic) │
|
|
429
|
+
│ │ │
|
|
430
|
+
│ └── NO → Is it shared across many components? │
|
|
431
|
+
│ ├── YES → Is it complex (many actions/reducers)? │
|
|
432
|
+
│ │ ├── YES → Zustand or Redux Toolkit │
|
|
433
|
+
│ │ └── NO → Zustand (lightweight) or Jotai (atomic) │
|
|
434
|
+
│ │ │
|
|
435
|
+
│ └── NO → Is it just a toggle/input/form? │
|
|
436
|
+
│ ├── YES → useState / useReducer (local) │
|
|
437
|
+
│ └── Is it URL-dependent? │
|
|
438
|
+
│ └── YES → useSearchParams / nuqs │
|
|
439
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Zustand (Recommended Default)
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
import { create } from "zustand";
|
|
446
|
+
import { devtools, persist } from "zustand/middleware";
|
|
447
|
+
|
|
448
|
+
interface CartStore {
|
|
449
|
+
items: CartItem[];
|
|
450
|
+
total: number;
|
|
451
|
+
addItem: (item: CartItem) => void;
|
|
452
|
+
removeItem: (id: string) => void;
|
|
453
|
+
clearCart: () => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const useCartStore = create<CartStore>()(
|
|
457
|
+
devtools(
|
|
458
|
+
persist(
|
|
459
|
+
(set, get) => ({
|
|
460
|
+
items: [],
|
|
461
|
+
total: 0,
|
|
462
|
+
|
|
463
|
+
addItem: (item) =>
|
|
464
|
+
set((state) => ({
|
|
465
|
+
items: [...state.items, item],
|
|
466
|
+
total: state.total + item.price,
|
|
467
|
+
})),
|
|
468
|
+
|
|
469
|
+
removeItem: (id) =>
|
|
470
|
+
set((state) => ({
|
|
471
|
+
items: state.items.filter((i) => i.id !== id),
|
|
472
|
+
total: state.items
|
|
473
|
+
.filter((i) => i.id !== id)
|
|
474
|
+
.reduce((sum, i) => sum + i.price, 0),
|
|
475
|
+
})),
|
|
476
|
+
|
|
477
|
+
clearCart: () => set({ items: [], total: 0 }),
|
|
478
|
+
}),
|
|
479
|
+
{ name: "cart-storage" } // localStorage key
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Usage in component:
|
|
485
|
+
function CartIcon() {
|
|
486
|
+
// ✅ Selector — only re-renders when items.length changes
|
|
487
|
+
const count = useCartStore((state) => state.items.length);
|
|
488
|
+
return <span className="badge">{count}</span>;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ❌ HALLUCINATION TRAP: Always use selectors with Zustand
|
|
492
|
+
// useCartStore() without a selector subscribes to EVERYTHING
|
|
493
|
+
// and causes unnecessary re-renders on any store change
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### TanStack React Query (Server State)
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
500
|
+
|
|
501
|
+
function UserList() {
|
|
502
|
+
const { data, isLoading, error } = useQuery({
|
|
503
|
+
queryKey: ["users"],
|
|
504
|
+
queryFn: () => fetch("/api/users").then((r) => r.json()),
|
|
505
|
+
staleTime: 5 * 60 * 1000, // 5 min before refetch
|
|
506
|
+
gcTime: 10 * 60 * 1000, // 10 min cache lifetime
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (isLoading) return <Skeleton />;
|
|
510
|
+
if (error) return <ErrorDisplay error={error} />;
|
|
511
|
+
|
|
512
|
+
return data.map((user: User) => <UserCard key={user.id} user={user} />);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Mutation with optimistic update
|
|
516
|
+
function useDeleteUser() {
|
|
517
|
+
const queryClient = useQueryClient();
|
|
518
|
+
|
|
519
|
+
return useMutation({
|
|
520
|
+
mutationFn: (userId: string) =>
|
|
521
|
+
fetch(`/api/users/${userId}`, { method: "DELETE" }),
|
|
522
|
+
|
|
523
|
+
onMutate: async (userId) => {
|
|
524
|
+
await queryClient.cancelQueries({ queryKey: ["users"] });
|
|
525
|
+
const previous = queryClient.getQueryData<User[]>(["users"]);
|
|
526
|
+
|
|
527
|
+
queryClient.setQueryData<User[]>(["users"], (old) =>
|
|
528
|
+
old?.filter((u) => u.id !== userId)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
return { previous }; // context for rollback
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
onError: (_err, _userId, context) => {
|
|
535
|
+
queryClient.setQueryData(["users"], context?.previous); // rollback
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
onSettled: () => {
|
|
539
|
+
queryClient.invalidateQueries({ queryKey: ["users"] }); // refetch
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ❌ HALLUCINATION TRAP: `cacheTime` was renamed to `gcTime` in React Query v5
|
|
545
|
+
// ❌ HALLUCINATION TRAP: Import from "@tanstack/react-query", NOT "react-query"
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Performance Optimization
|
|
551
|
+
|
|
552
|
+
### Code Splitting & Lazy Loading
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
import { lazy, Suspense } from "react";
|
|
556
|
+
|
|
557
|
+
// Lazy load heavy components
|
|
558
|
+
const HeavyChart = lazy(() => import("./HeavyChart"));
|
|
559
|
+
const AdminPanel = lazy(() => import("./AdminPanel"));
|
|
560
|
+
|
|
561
|
+
function App() {
|
|
562
|
+
return (
|
|
563
|
+
<Suspense fallback={<Skeleton />}>
|
|
564
|
+
<HeavyChart />
|
|
565
|
+
</Suspense>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Named exports require:
|
|
570
|
+
const Chart = lazy(() =>
|
|
571
|
+
import("./Charts").then((mod) => ({ default: mod.BarChart }))
|
|
572
|
+
);
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Virtual Scrolling (Large Lists)
|
|
576
|
+
|
|
577
|
+
```tsx
|
|
578
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
579
|
+
|
|
580
|
+
function VirtualList({ items }: { items: Item[] }) {
|
|
581
|
+
const parentRef = useRef<HTMLDivElement>(null);
|
|
582
|
+
|
|
583
|
+
const virtualizer = useVirtualizer({
|
|
584
|
+
count: items.length,
|
|
585
|
+
getScrollElement: () => parentRef.current,
|
|
586
|
+
estimateSize: () => 50, // estimated row height in px
|
|
587
|
+
overscan: 5, // render 5 extra items above/below viewport
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
|
|
592
|
+
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
|
|
593
|
+
{virtualizer.getVirtualItems().map((virtualRow) => (
|
|
594
|
+
<div
|
|
595
|
+
key={virtualRow.key}
|
|
596
|
+
style={{
|
|
597
|
+
position: "absolute",
|
|
598
|
+
top: virtualRow.start,
|
|
599
|
+
width: "100%",
|
|
600
|
+
height: virtualRow.size,
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
{items[virtualRow.index].name}
|
|
604
|
+
</div>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Use when: list has 500+ items
|
|
612
|
+
// Do NOT use for: lists under 100 items (adds complexity for no gain)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Ref Patterns
|
|
616
|
+
|
|
617
|
+
```tsx
|
|
618
|
+
// useRef for DOM access and mutable values
|
|
619
|
+
function VideoPlayer() {
|
|
620
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
621
|
+
|
|
622
|
+
function handlePlay() {
|
|
623
|
+
videoRef.current?.play();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return <video ref={videoRef} src="/movie.mp4" />;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Callback refs for dynamic ref assignment
|
|
630
|
+
function MeasuredBox() {
|
|
631
|
+
const [height, setHeight] = useState(0);
|
|
632
|
+
|
|
633
|
+
const measuredRef = useCallback((node: HTMLDivElement | null) => {
|
|
634
|
+
if (node) {
|
|
635
|
+
setHeight(node.getBoundingClientRect().height);
|
|
636
|
+
}
|
|
637
|
+
}, []);
|
|
638
|
+
|
|
639
|
+
return <div ref={measuredRef}>Content with height: {height}</div>;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Forwarding refs (for library components)
|
|
643
|
+
const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
644
|
+
function TextInput(props, ref) {
|
|
645
|
+
return <input ref={ref} {...props} />;
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// React 19: ref is a regular prop — forwardRef is being phased out
|
|
650
|
+
function TextInput19({ ref, ...props }: InputProps & { ref?: Ref<HTMLInputElement> }) {
|
|
651
|
+
return <input ref={ref} {...props} />;
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## TypeScript Patterns
|
|
658
|
+
|
|
659
|
+
### Component Props
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
// Discriminated union props
|
|
663
|
+
type ButtonProps =
|
|
664
|
+
| { variant: "link"; href: string; onClick?: never }
|
|
665
|
+
| { variant: "button"; onClick: () => void; href?: never };
|
|
666
|
+
|
|
667
|
+
function Button(props: ButtonProps) {
|
|
668
|
+
if (props.variant === "link") {
|
|
669
|
+
return <a href={props.href}>Link</a>;
|
|
670
|
+
}
|
|
671
|
+
return <button onClick={props.onClick}>Button</button>;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Polymorphic component (as prop)
|
|
675
|
+
type PolymorphicProps<E extends React.ElementType> = {
|
|
676
|
+
as?: E;
|
|
677
|
+
children: React.ReactNode;
|
|
678
|
+
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
|
|
679
|
+
|
|
680
|
+
function Text<E extends React.ElementType = "span">({
|
|
681
|
+
as,
|
|
682
|
+
children,
|
|
683
|
+
...props
|
|
684
|
+
}: PolymorphicProps<E>) {
|
|
685
|
+
const Component = as || "span";
|
|
686
|
+
return <Component {...props}>{children}</Component>;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Usage:
|
|
690
|
+
<Text as="h1" className="title">Heading</Text>
|
|
691
|
+
<Text as="p">Paragraph</Text>
|
|
692
|
+
<Text as="a" href="/about">Link</Text>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Generic Components
|
|
696
|
+
|
|
697
|
+
```tsx
|
|
698
|
+
// Generic list component
|
|
699
|
+
interface SelectProps<T> {
|
|
700
|
+
items: T[];
|
|
701
|
+
selected: T | null;
|
|
702
|
+
onSelect: (item: T) => void;
|
|
703
|
+
getLabel: (item: T) => string;
|
|
704
|
+
getKey: (item: T) => string;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function Select<T>({ items, selected, onSelect, getLabel, getKey }: SelectProps<T>) {
|
|
708
|
+
return (
|
|
709
|
+
<ul role="listbox">
|
|
710
|
+
{items.map((item) => (
|
|
711
|
+
<li
|
|
712
|
+
key={getKey(item)}
|
|
713
|
+
role="option"
|
|
714
|
+
aria-selected={item === selected}
|
|
715
|
+
onClick={() => onSelect(item)}
|
|
716
|
+
>
|
|
717
|
+
{getLabel(item)}
|
|
718
|
+
</li>
|
|
719
|
+
))}
|
|
720
|
+
</ul>
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Accessibility (Mandatory)
|
|
728
|
+
|
|
729
|
+
```tsx
|
|
730
|
+
// Every interactive element MUST be accessible
|
|
731
|
+
|
|
732
|
+
// ✅ Keyboard navigation
|
|
733
|
+
<button onClick={handleClick} onKeyDown={(e) => {
|
|
734
|
+
if (e.key === "Enter" || e.key === " ") handleClick();
|
|
735
|
+
}}>
|
|
736
|
+
Action
|
|
737
|
+
</button>
|
|
738
|
+
|
|
739
|
+
// ✅ ARIA for custom components
|
|
740
|
+
<div
|
|
741
|
+
role="dialog"
|
|
742
|
+
aria-modal={true}
|
|
743
|
+
aria-labelledby="dialog-title"
|
|
744
|
+
aria-describedby="dialog-desc"
|
|
745
|
+
>
|
|
746
|
+
<h2 id="dialog-title">Confirm Delete</h2>
|
|
747
|
+
<p id="dialog-desc">This action cannot be undone.</p>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
// ✅ Focus management
|
|
751
|
+
function Modal({ isOpen, onClose }: ModalProps) {
|
|
752
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
753
|
+
|
|
754
|
+
useEffect(() => {
|
|
755
|
+
if (isOpen) closeRef.current?.focus(); // focus trap
|
|
756
|
+
}, [isOpen]);
|
|
757
|
+
|
|
758
|
+
return isOpen ? (
|
|
759
|
+
<div role="dialog" aria-modal>
|
|
760
|
+
<button ref={closeRef} onClick={onClose}>Close</button>
|
|
761
|
+
</div>
|
|
762
|
+
) : null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ✅ Screen reader text
|
|
766
|
+
<button aria-label="Close dialog">
|
|
767
|
+
<XIcon aria-hidden="true" />
|
|
768
|
+
</button>
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## Common Patterns
|
|
774
|
+
|
|
775
|
+
### Debounced Search Input
|
|
776
|
+
|
|
777
|
+
```tsx
|
|
778
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
779
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
780
|
+
|
|
781
|
+
useEffect(() => {
|
|
782
|
+
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
|
783
|
+
return () => clearTimeout(timer);
|
|
784
|
+
}, [value, delay]);
|
|
785
|
+
|
|
786
|
+
return debouncedValue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function SearchInput() {
|
|
790
|
+
const [query, setQuery] = useState("");
|
|
791
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
792
|
+
|
|
793
|
+
useEffect(() => {
|
|
794
|
+
if (debouncedQuery) {
|
|
795
|
+
searchAPI(debouncedQuery);
|
|
796
|
+
}
|
|
797
|
+
}, [debouncedQuery]);
|
|
798
|
+
|
|
799
|
+
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Intersection Observer Hook
|
|
804
|
+
|
|
805
|
+
```tsx
|
|
806
|
+
function useIntersectionObserver(
|
|
807
|
+
options?: IntersectionObserverInit
|
|
808
|
+
): [React.RefCallback<Element>, boolean] {
|
|
809
|
+
const [isIntersecting, setIsIntersecting] = useState(false);
|
|
810
|
+
|
|
811
|
+
const ref = useCallback(
|
|
812
|
+
(node: Element | null) => {
|
|
813
|
+
if (!node) return;
|
|
814
|
+
|
|
815
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
816
|
+
setIsIntersecting(entry.isIntersecting);
|
|
817
|
+
}, options);
|
|
818
|
+
|
|
819
|
+
observer.observe(node);
|
|
820
|
+
return () => observer.disconnect();
|
|
821
|
+
},
|
|
822
|
+
[options]
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
return [ref, isIntersecting];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Usage:
|
|
829
|
+
function LazyImage({ src }: { src: string }) {
|
|
830
|
+
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 });
|
|
831
|
+
return <div ref={ref}>{isVisible && <img src={src} />}</div>;
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Previous Value Hook
|
|
836
|
+
|
|
837
|
+
```tsx
|
|
838
|
+
function usePrevious<T>(value: T): T | undefined {
|
|
839
|
+
const ref = useRef<T>(undefined);
|
|
840
|
+
|
|
841
|
+
useEffect(() => {
|
|
842
|
+
ref.current = value;
|
|
843
|
+
}, [value]);
|
|
844
|
+
|
|
845
|
+
return ref.current;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Usage: detect state changes
|
|
849
|
+
function Counter({ count }: { count: number }) {
|
|
850
|
+
const prevCount = usePrevious(count);
|
|
851
|
+
const direction = prevCount !== undefined && count > prevCount ? "↑" : "↓";
|
|
852
|
+
|
|
853
|
+
return <span>{direction} {count}</span>;
|
|
854
|
+
}
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
---
|
|
858
|
+
|
|
859
|
+
## Output Format
|
|
860
|
+
|
|
861
|
+
When this skill produces or reviews code, structure your output as follows:
|
|
862
|
+
|
|
863
|
+
```
|
|
864
|
+
━━━ React Specialist Report ━━━━━━━━━━━━━━━━━━━━━━━━
|
|
865
|
+
Skill: React Specialist
|
|
866
|
+
React Ver: 19+
|
|
867
|
+
Scope: [N files · N components]
|
|
868
|
+
─────────────────────────────────────────────────
|
|
869
|
+
✅ Passed: [checks that passed, or "All clean"]
|
|
870
|
+
⚠️ Warnings: [non-blocking issues, or "None"]
|
|
871
|
+
❌ Blocked: [blocking issues requiring fix, or "None"]
|
|
872
|
+
─────────────────────────────────────────────────
|
|
873
|
+
VBC status: PENDING → VERIFIED
|
|
874
|
+
Evidence: [test output / lint pass / compile success]
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**VBC (Verification-Before-Completion) is mandatory.**
|
|
878
|
+
Do not mark status as VERIFIED until concrete terminal evidence is provided.
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
## 🤖 LLM-Specific Traps
|
|
883
|
+
|
|
884
|
+
AI coding assistants often fall into specific bad habits when generating React code. These are strictly forbidden:
|
|
885
|
+
|
|
886
|
+
1. **Class Components:** Never generate `class extends React.Component` or lifecycle methods (`componentDidMount`, `componentDidUpdate`) in React 19+ projects. Use functional components with hooks exclusively.
|
|
887
|
+
2. **Manual Memoization in React 19:** Do NOT add `useMemo`, `useCallback`, or `React.memo` if the React Compiler is enabled. The compiler handles this automatically.
|
|
888
|
+
3. **`useFormState` (Wrong Name):** The correct hook name is `useActionState`, not `useFormState`. The canary name was changed before stable release.
|
|
889
|
+
4. **`<Context.Provider>`:** React 19 uses `<Context value={}>` directly. The `.Provider` pattern is deprecated.
|
|
890
|
+
5. **`useEffect` for Data Fetching:** Use Server Components, React Query, SWR, or the `use()` hook. `useEffect` fetch patterns cause waterfalls, have no caching, and lack error/loading states.
|
|
891
|
+
6. **Missing Keys in Mapped Lists:** Always use unique, stable IDs as keys. Never use array index as a key unless the list is truly static and never reorders.
|
|
892
|
+
7. **Prop Drilling Past 3 Levels:** If passing props through more than 3 intermediate components, use Context, Zustand, or Jotai instead.
|
|
893
|
+
8. **`cacheTime` in React Query v5:** The property was renamed to `gcTime`. Importing from `"react-query"` instead of `"@tanstack/react-query"` is also wrong.
|
|
894
|
+
9. **Zustand Without Selectors:** `useStore()` without a selector subscribes to all state changes. Always use `useStore((state) => state.specificValue)`.
|
|
895
|
+
10. **`forwardRef` in React 19:** Refs are regular props in React 19. `forwardRef` is being phased out. Use `ref` as a normal prop.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## 🏛️ Tribunal Integration (Anti-Hallucination)
|
|
900
|
+
|
|
901
|
+
**Slash command: `/tribunal-frontend`**
|
|
902
|
+
**Active reviewers: `logic` · `security` · `frontend` · `type-safety`**
|
|
903
|
+
|
|
904
|
+
### ❌ Forbidden AI Tropes
|
|
905
|
+
|
|
906
|
+
1. **Blind Assumptions:** Never make an assumption without documenting it clearly with `// VERIFY: [reason]`.
|
|
907
|
+
2. **Silent Degradation:** Catching and suppressing errors without logging or displaying error boundaries.
|
|
908
|
+
3. **Context Amnesia:** Forgetting the user's React version or framework constraints.
|
|
909
|
+
4. **Sloppy Layout Generation:** Never build UI without explicit dimensional boundaries — use strict 4px grid spacing and explicit flex/grid layouts.
|
|
910
|
+
|
|
911
|
+
### ✅ Pre-Flight Self-Audit
|
|
912
|
+
|
|
913
|
+
Review these questions before confirming output:
|
|
914
|
+
```
|
|
915
|
+
✅ Did I use strictly functional components with hooks?
|
|
916
|
+
✅ Did I avoid manual memoization if React Compiler is active?
|
|
917
|
+
✅ Did I use useActionState (not useFormState) for form actions?
|
|
918
|
+
✅ Did I use <Context value={}> (not <Context.Provider>)?
|
|
919
|
+
✅ Are array maps using unique, stable keys (not index)?
|
|
920
|
+
✅ Did I handle loading, error, and empty states?
|
|
921
|
+
✅ Did I use Suspense + Error Boundaries for async components?
|
|
922
|
+
✅ Is the component accessible (ARIA, keyboard, focus mgmt)?
|
|
923
|
+
✅ Did I include AbortController cleanup in useEffect fetches?
|
|
924
|
+
✅ Did I use Zustand selectors to prevent unnecessary re-renders?
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### 🛑 Verification-Before-Completion (VBC) Protocol
|
|
928
|
+
|
|
929
|
+
**CRITICAL:** You must follow a strict "evidence-based closeout" state machine.
|
|
930
|
+
- ❌ **Forbidden:** Assuming a React component "works" just because it compiles or because the bundler gives no immediate warnings.
|
|
931
|
+
- ✅ **Required:** You are explicitly forbidden from completing your task without providing **concrete terminal/test evidence** (e.g., passing Jest/Vitest logs, successful build output, or specific CLI execution results) proving the build is error-free.
|