tribunal-kit 3.0.0 → 4.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/ARCHITECTURE.md +99 -99
- package/.agent/GEMINI.md +52 -52
- package/.agent/agents/accessibility-reviewer.md +187 -220
- package/.agent/agents/ai-code-reviewer.md +199 -233
- package/.agent/agents/backend-specialist.md +215 -238
- package/.agent/agents/code-archaeologist.md +161 -181
- package/.agent/agents/database-architect.md +184 -207
- package/.agent/agents/debugger.md +191 -218
- package/.agent/agents/dependency-reviewer.md +103 -136
- package/.agent/agents/devops-engineer.md +218 -238
- package/.agent/agents/documentation-writer.md +201 -221
- package/.agent/agents/explorer-agent.md +160 -180
- package/.agent/agents/frontend-reviewer.md +160 -194
- package/.agent/agents/frontend-specialist.md +248 -237
- package/.agent/agents/game-developer.md +48 -52
- package/.agent/agents/logic-reviewer.md +116 -149
- package/.agent/agents/mobile-developer.md +200 -223
- package/.agent/agents/mobile-reviewer.md +162 -195
- package/.agent/agents/orchestrator.md +181 -211
- package/.agent/agents/penetration-tester.md +157 -174
- package/.agent/agents/performance-optimizer.md +183 -203
- package/.agent/agents/performance-reviewer.md +178 -211
- package/.agent/agents/precedence-reviewer.md +213 -0
- package/.agent/agents/product-manager.md +142 -162
- package/.agent/agents/product-owner.md +6 -25
- package/.agent/agents/project-planner.md +142 -162
- package/.agent/agents/qa-automation-engineer.md +225 -242
- package/.agent/agents/security-auditor.md +174 -194
- package/.agent/agents/seo-specialist.md +193 -213
- package/.agent/agents/sql-reviewer.md +161 -194
- package/.agent/agents/supervisor-agent.md +184 -203
- package/.agent/agents/swarm-worker-contracts.md +17 -17
- package/.agent/agents/swarm-worker-registry.md +46 -46
- package/.agent/agents/test-coverage-reviewer.md +160 -193
- package/.agent/agents/test-engineer.md +0 -21
- package/.agent/agents/type-safety-reviewer.md +175 -208
- package/.agent/patterns/generator.md +9 -9
- package/.agent/patterns/inversion.md +12 -12
- package/.agent/patterns/pipeline.md +9 -9
- package/.agent/patterns/reviewer.md +13 -13
- package/.agent/patterns/tool-wrapper.md +9 -9
- package/.agent/rules/GEMINI.md +63 -63
- package/.agent/scripts/append_flow.js +72 -0
- package/.agent/scripts/case_law_manager.py +525 -0
- package/.agent/scripts/compress_skills.py +167 -0
- package/.agent/scripts/consolidate_skills.py +173 -0
- package/.agent/scripts/deep_compress.py +202 -0
- package/.agent/scripts/minify_context.py +80 -0
- package/.agent/scripts/security_scan.py +1 -1
- package/.agent/scripts/skill_evolution.py +563 -0
- package/.agent/scripts/strip_tribunal.py +41 -0
- package/.agent/skills/agent-organizer/SKILL.md +100 -126
- package/.agent/skills/agentic-patterns/SKILL.md +0 -70
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +134 -160
- package/.agent/skills/api-patterns/SKILL.md +123 -215
- package/.agent/skills/api-security-auditor/SKILL.md +143 -177
- package/.agent/skills/app-builder/SKILL.md +334 -50
- package/.agent/skills/app-builder/templates/SKILL.md +13 -15
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +16 -16
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +22 -22
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +20 -20
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +17 -17
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +21 -21
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +20 -20
- package/.agent/skills/appflow-wireframe/SKILL.md +95 -121
- package/.agent/skills/architecture/SKILL.md +169 -331
- package/.agent/skills/authentication-best-practices/SKILL.md +139 -173
- package/.agent/skills/bash-linux/SKILL.md +129 -154
- package/.agent/skills/behavioral-modes/SKILL.md +8 -69
- package/.agent/skills/brainstorming/SKILL.md +436 -104
- package/.agent/skills/building-native-ui/SKILL.md +152 -174
- package/.agent/skills/clean-code/SKILL.md +331 -360
- package/.agent/skills/code-review-checklist/SKILL.md +0 -62
- package/.agent/skills/config-validator/SKILL.md +115 -141
- package/.agent/skills/csharp-developer/SKILL.md +468 -528
- package/.agent/skills/database-design/SKILL.md +104 -369
- package/.agent/skills/deployment-procedures/SKILL.md +119 -145
- package/.agent/skills/devops-engineer/SKILL.md +295 -332
- package/.agent/skills/devops-incident-responder/SKILL.md +87 -113
- package/.agent/skills/doc.md +5 -5
- package/.agent/skills/documentation-templates/SKILL.md +27 -63
- package/.agent/skills/edge-computing/SKILL.md +131 -157
- package/.agent/skills/extract-design-system/SKILL.md +108 -134
- package/.agent/skills/framer-motion-expert/SKILL.md +111 -855
- package/.agent/skills/frontend-design/SKILL.md +151 -499
- package/.agent/skills/game-design-expert/SKILL.md +79 -105
- package/.agent/skills/game-engineering-expert/SKILL.md +96 -122
- package/.agent/skills/geo-fundamentals/SKILL.md +97 -124
- package/.agent/skills/github-operations/SKILL.md +279 -314
- package/.agent/skills/gsap-expert/SKILL.md +119 -826
- package/.agent/skills/i18n-localization/SKILL.md +113 -138
- package/.agent/skills/intelligent-routing/SKILL.md +167 -127
- package/.agent/skills/lint-and-validate/SKILL.md +16 -52
- package/.agent/skills/llm-engineering/SKILL.md +344 -357
- package/.agent/skills/local-first/SKILL.md +128 -154
- package/.agent/skills/mcp-builder/SKILL.md +92 -118
- package/.agent/skills/mobile-design/SKILL.md +213 -219
- package/.agent/skills/motion-engineering/SKILL.md +184 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +99 -698
- package/.agent/skills/nodejs-best-practices/SKILL.md +498 -559
- package/.agent/skills/observability/SKILL.md +293 -330
- package/.agent/skills/parallel-agents/SKILL.md +96 -122
- package/.agent/skills/performance-profiling/SKILL.md +217 -254
- package/.agent/skills/plan-writing/SKILL.md +92 -118
- package/.agent/skills/platform-engineer/SKILL.md +97 -123
- package/.agent/skills/playwright-best-practices/SKILL.md +137 -162
- package/.agent/skills/powershell-windows/SKILL.md +112 -146
- package/.agent/skills/project-idioms/SKILL.md +87 -0
- package/.agent/skills/python-patterns/SKILL.md +15 -35
- package/.agent/skills/python-pro/SKILL.md +148 -754
- package/.agent/skills/react-specialist/SKILL.md +123 -827
- package/.agent/skills/readme-builder/SKILL.md +23 -85
- package/.agent/skills/realtime-patterns/SKILL.md +269 -304
- package/.agent/skills/red-team-tactics/SKILL.md +18 -51
- package/.agent/skills/rust-pro/SKILL.md +623 -701
- package/.agent/skills/seo-fundamentals/SKILL.md +129 -154
- package/.agent/skills/server-management/SKILL.md +164 -190
- package/.agent/skills/shadcn-ui-expert/SKILL.md +181 -206
- package/.agent/skills/skill-creator/SKILL.md +24 -56
- package/.agent/skills/sql-pro/SKILL.md +579 -633
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +35 -66
- package/.agent/skills/swiftui-expert/SKILL.md +151 -176
- package/.agent/skills/systematic-debugging/SKILL.md +92 -118
- package/.agent/skills/tailwind-patterns/SKILL.md +516 -576
- package/.agent/skills/tdd-workflow/SKILL.md +111 -137
- package/.agent/skills/test-result-analyzer/SKILL.md +33 -73
- package/.agent/skills/testing-patterns/SKILL.md +512 -573
- package/.agent/skills/trend-researcher/SKILL.md +30 -71
- package/.agent/skills/ui-ux-pro-max/SKILL.md +8 -41
- package/.agent/skills/ui-ux-researcher/SKILL.md +51 -91
- package/.agent/skills/vue-expert/SKILL.md +127 -866
- package/.agent/skills/vulnerability-scanner/SKILL.md +354 -269
- package/.agent/skills/web-accessibility-auditor/SKILL.md +168 -193
- package/.agent/skills/web-design-guidelines/SKILL.md +25 -61
- package/.agent/skills/webapp-testing/SKILL.md +119 -145
- package/.agent/skills/whimsy-injector/SKILL.md +58 -132
- package/.agent/skills/workflow-optimizer/SKILL.md +28 -68
- package/.agent/workflows/api-tester.md +151 -151
- package/.agent/workflows/audit.md +127 -138
- package/.agent/workflows/brainstorm.md +110 -110
- package/.agent/workflows/changelog.md +112 -112
- package/.agent/workflows/create.md +124 -124
- package/.agent/workflows/debug.md +165 -189
- package/.agent/workflows/deploy.md +180 -189
- package/.agent/workflows/enhance.md +128 -151
- package/.agent/workflows/fix.md +114 -135
- package/.agent/workflows/generate.md +13 -4
- package/.agent/workflows/migrate.md +160 -160
- package/.agent/workflows/orchestrate.md +168 -168
- package/.agent/workflows/performance-benchmarker.md +114 -123
- package/.agent/workflows/plan.md +173 -173
- package/.agent/workflows/preview.md +80 -80
- package/.agent/workflows/refactor.md +161 -183
- package/.agent/workflows/review-ai.md +101 -129
- package/.agent/workflows/review.md +116 -116
- package/.agent/workflows/session.md +94 -94
- package/.agent/workflows/status.md +79 -79
- package/.agent/workflows/strengthen-skills.md +138 -139
- package/.agent/workflows/swarm.md +179 -179
- package/.agent/workflows/test.md +189 -211
- package/.agent/workflows/tribunal-backend.md +94 -113
- package/.agent/workflows/tribunal-database.md +95 -115
- package/.agent/workflows/tribunal-frontend.md +96 -118
- package/.agent/workflows/tribunal-full.md +93 -133
- package/.agent/workflows/tribunal-mobile.md +95 -119
- package/.agent/workflows/tribunal-performance.md +110 -133
- package/.agent/workflows/ui-ux-pro-max.md +122 -143
- package/README.md +30 -1
- package/bin/tribunal-kit.js +175 -12
- package/package.json +25 -4
- package/.agent/skills/api-patterns/api-style.md +0 -42
- package/.agent/skills/api-patterns/auth.md +0 -24
- package/.agent/skills/api-patterns/documentation.md +0 -26
- package/.agent/skills/api-patterns/graphql.md +0 -41
- package/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/.agent/skills/api-patterns/response.md +0 -37
- package/.agent/skills/api-patterns/rest.md +0 -40
- package/.agent/skills/api-patterns/security-testing.md +0 -122
- package/.agent/skills/api-patterns/trpc.md +0 -41
- package/.agent/skills/api-patterns/versioning.md +0 -22
- package/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/.agent/skills/app-builder/feature-building.md +0 -53
- package/.agent/skills/app-builder/project-detection.md +0 -34
- package/.agent/skills/app-builder/scaffolding.md +0 -118
- package/.agent/skills/app-builder/tech-stack.md +0 -40
- package/.agent/skills/architecture/context-discovery.md +0 -43
- package/.agent/skills/architecture/examples.md +0 -94
- package/.agent/skills/architecture/pattern-selection.md +0 -68
- package/.agent/skills/architecture/patterns-reference.md +0 -50
- package/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/.agent/skills/brainstorming/dynamic-questioning.md +0 -360
- package/.agent/skills/database-design/database-selection.md +0 -43
- package/.agent/skills/database-design/indexing.md +0 -39
- package/.agent/skills/database-design/migrations.md +0 -48
- package/.agent/skills/database-design/optimization.md +0 -36
- package/.agent/skills/database-design/orm-selection.md +0 -30
- package/.agent/skills/database-design/schema-design.md +0 -56
- package/.agent/skills/frontend-design/animation-guide.md +0 -331
- package/.agent/skills/frontend-design/color-system.md +0 -329
- package/.agent/skills/frontend-design/decision-trees.md +0 -418
- package/.agent/skills/frontend-design/motion-graphics.md +0 -306
- package/.agent/skills/frontend-design/typography-system.md +0 -363
- package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
- package/.agent/skills/frontend-design/visual-effects.md +0 -383
- package/.agent/skills/intelligent-routing/router-manifest.md +0 -65
- package/.agent/skills/mobile-design/decision-trees.md +0 -516
- package/.agent/skills/mobile-design/mobile-backend.md +0 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +0 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +0 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +0 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +0 -458
- package/.agent/skills/mobile-design/mobile-performance.md +0 -767
- package/.agent/skills/mobile-design/mobile-testing.md +0 -356
- package/.agent/skills/mobile-design/mobile-typography.md +0 -433
- package/.agent/skills/mobile-design/platform-android.md +0 -666
- package/.agent/skills/mobile-design/platform-ios.md +0 -561
- package/.agent/skills/mobile-design/touch-psychology.md +0 -537
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +0 -312
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +0 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +0 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +0 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +0 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +0 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +0 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +0 -150
- package/.agent/skills/vulnerability-scanner/checklists.md +0 -121
|
@@ -1,931 +1,227 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: react-specialist
|
|
3
|
-
description:
|
|
3
|
+
description: React 19+ specialist. use(), useActionState, useOptimistic, React Compiler, Server/Client Components, Zustand/Jotai, React Query. Use when building components, managing state, optimizing renders.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Glob, Grep
|
|
5
|
-
version:
|
|
6
|
-
last-updated: 2026-
|
|
7
|
-
applies-to-model: gemini-2.5-pro, claude-3-7-sonnet
|
|
5
|
+
version: 3.1.0
|
|
6
|
+
last-updated: 2026-04-06
|
|
8
7
|
---
|
|
9
8
|
|
|
10
|
-
# React
|
|
9
|
+
# React 19+ — Dense Reference
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
## Hallucination Traps (Read First)
|
|
12
|
+
- ❌ `useFormState` → ✅ `useActionState` (stable name)
|
|
13
|
+
- ❌ `useContext()` in conditionals → ✅ `use(Context)` CAN be conditional
|
|
14
|
+
- ❌ `useMemo/useCallback/React.memo` in React 19+ projects → ✅ React Compiler handles this
|
|
15
|
+
- ❌ `exitBeforeEnter` (Framer) → ✅ `mode="wait"` on `<AnimatePresence>`
|
|
16
|
+
- ❌ `next/router` → ✅ `next/navigation` in App Router
|
|
17
|
+
- ❌ Server Components using `useState/useEffect` → must be `"use client"`
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
16
|
-
## React 19 Core
|
|
17
|
-
|
|
18
|
-
### The `use()` Hook
|
|
21
|
+
## React 19 Core APIs
|
|
19
22
|
|
|
23
|
+
### `use()` — Replaces many useEffect patterns
|
|
20
24
|
```tsx
|
|
21
|
-
// use() can read promises and context — replaces many useEffect patterns
|
|
22
25
|
import { use } from "react";
|
|
23
|
-
|
|
26
|
+
// Reads promises (suspends until resolved)
|
|
24
27
|
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
|
|
25
|
-
const user = use(userPromise); // suspends
|
|
28
|
+
const user = use(userPromise); // suspends
|
|
26
29
|
return <h1>{user.name}</h1>;
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (isAdmin) {
|
|
32
|
-
const theme = use(ThemeContext); // ✅ conditional context read
|
|
33
|
-
return <AdminPanel theme={theme} />;
|
|
34
|
-
}
|
|
31
|
+
// Reads context — CAN be called conditionally (unlike useContext)
|
|
32
|
+
function Admin({ isAdmin }: { isAdmin: boolean }) {
|
|
33
|
+
if (isAdmin) return <Panel theme={use(ThemeContext)} />;
|
|
35
34
|
return <PublicPanel />;
|
|
36
35
|
}
|
|
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
36
|
```
|
|
42
37
|
|
|
43
|
-
### `useActionState`
|
|
44
|
-
|
|
38
|
+
### `useActionState` — Form actions with state
|
|
45
39
|
```tsx
|
|
46
|
-
import { useActionState } from "react";
|
|
40
|
+
import { useActionState } from "react"; // NOT useFormState
|
|
47
41
|
|
|
48
42
|
async function submitForm(prevState: FormState, formData: FormData) {
|
|
49
43
|
const email = formData.get("email") as string;
|
|
50
|
-
|
|
51
|
-
if (!email.includes("@")) {
|
|
52
|
-
return { error: "Invalid email", success: false };
|
|
53
|
-
}
|
|
54
|
-
|
|
44
|
+
if (!email.includes("@")) return { error: "Invalid email" };
|
|
55
45
|
await saveToDatabase(email);
|
|
56
46
|
return { error: null, success: true };
|
|
57
47
|
}
|
|
58
48
|
|
|
59
49
|
function SignupForm() {
|
|
60
|
-
const [state, formAction, isPending] = useActionState(submitForm, {
|
|
61
|
-
error: null,
|
|
62
|
-
success: false,
|
|
63
|
-
});
|
|
64
|
-
|
|
50
|
+
const [state, formAction, isPending] = useActionState(submitForm, { error: null });
|
|
65
51
|
return (
|
|
66
52
|
<form action={formAction}>
|
|
67
|
-
<input name="email"
|
|
68
|
-
{state.error && <p
|
|
69
|
-
<button
|
|
70
|
-
{isPending ? "Submitting..." : "Sign Up"}
|
|
71
|
-
</button>
|
|
53
|
+
<input name="email" disabled={isPending} />
|
|
54
|
+
{state.error && <p>{state.error}</p>}
|
|
55
|
+
<button disabled={isPending}>{isPending ? "Saving..." : "Submit"}</button>
|
|
72
56
|
</form>
|
|
73
57
|
);
|
|
74
58
|
}
|
|
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
59
|
```
|
|
81
60
|
|
|
82
|
-
### `useOptimistic`
|
|
83
|
-
|
|
61
|
+
### `useOptimistic` — Instant UI feedback
|
|
84
62
|
```tsx
|
|
85
63
|
import { useOptimistic } from "react";
|
|
86
|
-
|
|
87
64
|
function TodoList({ todos }: { todos: Todo[] }) {
|
|
88
|
-
const [
|
|
65
|
+
const [optimistic, addOptimistic] = useOptimistic(
|
|
89
66
|
todos,
|
|
90
|
-
(
|
|
67
|
+
(current, newTodo: Todo) => [...current, newTodo]
|
|
91
68
|
);
|
|
92
|
-
|
|
93
69
|
async function handleAdd(formData: FormData) {
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
70
|
+
addOptimistic({ id: crypto.randomUUID(), title: formData.get("title"), pending: true });
|
|
71
|
+
await saveTodo(formData.get("title") as string);
|
|
101
72
|
}
|
|
102
|
-
|
|
103
73
|
return (
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
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>
|
|
74
|
+
<form action={handleAdd}>
|
|
75
|
+
{optimistic.map(t => <li key={t.id} style={{ opacity: t.pending ? 0.5 : 1 }}>{t.title}</li>)}
|
|
76
|
+
</form>
|
|
117
77
|
);
|
|
118
78
|
}
|
|
119
79
|
```
|
|
120
80
|
|
|
121
|
-
### `useFormStatus`
|
|
122
|
-
|
|
81
|
+
### `useFormStatus` — Button pending state (must be INSIDE `<form>`)
|
|
123
82
|
```tsx
|
|
124
83
|
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
|
-
|
|
84
|
+
// ❌ TRAP: Cannot be called in the same component as <form>
|
|
130
85
|
function SubmitButton() {
|
|
131
|
-
const { pending
|
|
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
|
-
);
|
|
86
|
+
const { pending } = useFormStatus(); // reads nearest parent form
|
|
87
|
+
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
|
|
148
88
|
}
|
|
149
89
|
```
|
|
150
90
|
|
|
151
|
-
### `useTransition`
|
|
152
|
-
|
|
91
|
+
### `useTransition` — Non-blocking updates (React 19: supports async)
|
|
153
92
|
```tsx
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
93
|
+
const [isPending, startTransition] = useTransition();
|
|
94
|
+
startTransition(async () => {
|
|
95
|
+
const data = await search(query); // async supported in React 19 only
|
|
96
|
+
setResults(data);
|
|
97
|
+
});
|
|
183
98
|
```
|
|
184
99
|
|
|
185
|
-
### `useDeferredValue`
|
|
186
|
-
|
|
100
|
+
### `useDeferredValue` — Stale-while-rerender
|
|
187
101
|
```tsx
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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);
|
|
102
|
+
const deferredQuery = useDeferredValue(query);
|
|
103
|
+
const isStale = query !== deferredQuery;
|
|
104
|
+
return <div style={{ opacity: isStale ? 0.6 : 1 }}><ExpensiveList query={deferredQuery} /></div>;
|
|
105
|
+
// React 19: useDeferredValue(value, initialFallback) — 2-arg form
|
|
205
106
|
```
|
|
206
107
|
|
|
207
108
|
---
|
|
208
109
|
|
|
209
|
-
##
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
```
|
|
110
|
+
## React Compiler (React 19)
|
|
111
|
+
- Auto-memoizes components/values/callbacks. **Don't manually memoize in React 19 projects.**
|
|
112
|
+
- ❌ `useMemo(() => calc(a), [a])` / `useCallback(fn, [id])` / `React.memo(Comp)` — legacy
|
|
113
|
+
- ✅ Write plain functions/values. Compiler optimizes automatically.
|
|
114
|
+
- Exception: still use manual memo if compiler is explicitly disabled in config.
|
|
249
115
|
|
|
250
116
|
---
|
|
251
117
|
|
|
252
|
-
## Component
|
|
253
|
-
|
|
254
|
-
### Compound Components
|
|
118
|
+
## Component Patterns
|
|
255
119
|
|
|
120
|
+
### Compound Components (shared state via context)
|
|
256
121
|
```tsx
|
|
257
|
-
|
|
258
|
-
const TabsContext = createContext<{
|
|
259
|
-
activeTab: string;
|
|
260
|
-
setActiveTab: (id: string) => void;
|
|
261
|
-
} | null>(null);
|
|
262
|
-
|
|
122
|
+
const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | null>(null);
|
|
263
123
|
function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
|
|
264
|
-
const [
|
|
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>;
|
|
124
|
+
const [active, setActive] = useState(defaultTab);
|
|
125
|
+
return <TabsContext value={{ active, setActive }}><div>{children}</div></TabsContext>;
|
|
274
126
|
}
|
|
275
|
-
|
|
276
127
|
function Tab({ id, children }: { id: string; children: ReactNode }) {
|
|
277
|
-
const ctx = use(TabsContext)
|
|
278
|
-
|
|
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
|
-
);
|
|
128
|
+
const ctx = use(TabsContext)!;
|
|
129
|
+
return <button onClick={() => ctx.setActive(id)} aria-selected={ctx.active === id}>{children}</button>;
|
|
289
130
|
}
|
|
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
131
|
```
|
|
312
132
|
|
|
313
|
-
###
|
|
133
|
+
### Render Props / Higher Order Hooks
|
|
134
|
+
Prefer custom hooks over render props for modern React.
|
|
314
135
|
|
|
136
|
+
### Context Performance Pattern
|
|
315
137
|
```tsx
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
}
|
|
138
|
+
// Split context — prevents all consumers from re-rendering on every change
|
|
139
|
+
const CountStateCtx = createContext<number>(0);
|
|
140
|
+
const CountDispatchCtx = createContext<Dispatch<Action>>(() => {});
|
|
415
141
|
```
|
|
416
142
|
|
|
417
143
|
---
|
|
418
144
|
|
|
419
|
-
## State Management
|
|
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)
|
|
145
|
+
## State Management
|
|
443
146
|
|
|
147
|
+
### Zustand (preferred for global state)
|
|
444
148
|
```tsx
|
|
445
149
|
import { create } from "zustand";
|
|
446
|
-
import {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
)
|
|
150
|
+
import { persist } from "zustand/middleware";
|
|
151
|
+
const useStore = create<Store>()(
|
|
152
|
+
persist(
|
|
153
|
+
(set, get) => ({
|
|
154
|
+
count: 0,
|
|
155
|
+
increment: () => set(s => ({ count: s.count + 1 })),
|
|
156
|
+
getDoubled: () => get().count * 2,
|
|
157
|
+
}),
|
|
158
|
+
{ name: "my-store" }
|
|
481
159
|
)
|
|
482
160
|
);
|
|
483
|
-
|
|
484
|
-
//
|
|
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
|
-
);
|
|
161
|
+
// ❌ TRAP: Do NOT destructure the whole store — causes re-render on every change
|
|
162
|
+
// ✅ const count = useStore(s => s.count); // selector
|
|
573
163
|
```
|
|
574
164
|
|
|
575
|
-
###
|
|
576
|
-
|
|
165
|
+
### Jotai (preferred for derived/atomic state)
|
|
577
166
|
```tsx
|
|
578
|
-
import {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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)
|
|
167
|
+
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
168
|
+
const countAtom = atom(0);
|
|
169
|
+
const doubledAtom = atom(get => get(countAtom) * 2); // derived atom
|
|
170
|
+
// ❌ TRAP: atomWithStorage is from jotai/utils — NOT from jotai
|
|
171
|
+
import { atomWithStorage } from "jotai/utils";
|
|
613
172
|
```
|
|
614
173
|
|
|
615
|
-
###
|
|
616
|
-
|
|
174
|
+
### React Query / TanStack Query
|
|
617
175
|
```tsx
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
}
|
|
176
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
177
|
+
const { data, isPending, error } = useQuery({
|
|
178
|
+
queryKey: ["user", userId],
|
|
179
|
+
queryFn: () => fetchUser(userId),
|
|
180
|
+
staleTime: 5 * 60 * 1000, // don't refetch for 5 minutes
|
|
181
|
+
});
|
|
182
|
+
// Optimistic update:
|
|
183
|
+
const qc = useQueryClient();
|
|
184
|
+
const mutation = useMutation({
|
|
185
|
+
mutationFn: updateUser,
|
|
186
|
+
onMutate: async (newUser) => {
|
|
187
|
+
await qc.cancelQueries({ queryKey: ["user", newUser.id] });
|
|
188
|
+
const prev = qc.getQueryData(["user", newUser.id]);
|
|
189
|
+
qc.setQueryData(["user", newUser.id], newUser);
|
|
190
|
+
return { prev };
|
|
191
|
+
},
|
|
192
|
+
onError: (_, __, ctx) => qc.setQueryData(["user"], ctx?.prev),
|
|
193
|
+
onSettled: () => qc.invalidateQueries({ queryKey: ["user"] }),
|
|
194
|
+
});
|
|
723
195
|
```
|
|
724
196
|
|
|
725
197
|
---
|
|
726
198
|
|
|
727
|
-
##
|
|
199
|
+
## Performance
|
|
728
200
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
```
|
|
201
|
+
| Technique | When |
|
|
202
|
+
|-----------|------|
|
|
203
|
+
| `useDeferredValue` | Expensive derived render (charts, filters) |
|
|
204
|
+
| `useTransition` | Page nav, tab switches, data load |
|
|
205
|
+
| `lazy()` + `Suspense` | Code-split heavy components |
|
|
206
|
+
| `<Virtuoso>` / `<WindowVirtualizer>` | Lists > 200 items |
|
|
207
|
+
| Avoid `useEffect` for state transforms | Use `useMemo` or derived atoms |
|
|
770
208
|
|
|
771
209
|
---
|
|
772
210
|
|
|
773
|
-
##
|
|
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
|
|
211
|
+
## Refs & DOM
|
|
836
212
|
|
|
837
213
|
```tsx
|
|
838
|
-
|
|
839
|
-
|
|
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>;
|
|
214
|
+
// React 19: ref is now a prop (no forwardRef needed)
|
|
215
|
+
function Input({ ref, ...props }: ComponentProps<"input"> & { ref?: Ref<HTMLInputElement> }) {
|
|
216
|
+
return <input ref={ref} {...props} />;
|
|
854
217
|
}
|
|
218
|
+
// ❌ TRAP: forwardRef is deprecated in React 19 — still works but not needed
|
|
855
219
|
```
|
|
856
220
|
|
|
857
221
|
---
|
|
858
222
|
|
|
859
|
-
##
|
|
860
|
-
|
|
861
|
-
|
|
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.
|
|
223
|
+
## Testing Checklist
|
|
224
|
+
- ✅ Use React Testing Library — test behavior, not implementation
|
|
225
|
+
- ✅ `userEvent` over `fireEvent` (async, closer to real interaction)
|
|
226
|
+
- ✅ Mock server calls with MSW (Mock Service Worker)
|
|
227
|
+
- ❌ Never test internal state, ref values, or component instances
|