@minhduydev/mdpi 0.4.0 → 0.5.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.
Files changed (48) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.pi/VERSION +1 -1
  3. package/dist/template/.pi/extensions/templates-injector.ts +34 -6
  4. package/dist/template/.pi/prompts/INDEX.md +3 -9
  5. package/dist/template/.pi/skills/INDEX.md +81 -19
  6. package/dist/template/.pi/skills/accessibility-audit/SKILL.md +8 -2
  7. package/dist/template/.pi/skills/baseline-ui/SKILL.md +211 -0
  8. package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
  9. package/dist/template/.pi/skills/design-taste-frontend/SKILL.md +53 -42
  10. package/dist/template/.pi/skills/fixing-accessibility/SKILL.md +509 -0
  11. package/dist/template/.pi/skills/frontend-design/SKILL.md +60 -47
  12. package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
  13. package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
  14. package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
  15. package/dist/template/.pi/skills/frontend-ui-engineering/SKILL.md +21 -27
  16. package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
  17. package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
  18. package/dist/template/.pi/skills/oklch-color-workflow/SKILL.md +426 -0
  19. package/dist/template/.pi/skills/production-hardening/SKILL.md +652 -0
  20. package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
  21. package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
  22. package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
  23. package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
  24. package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
  25. package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
  26. package/dist/template/.pi/skills/ui-craft-principles/SKILL.md +564 -0
  27. package/dist/template/.pi/skills/ui-quality-audit/SKILL.md +329 -0
  28. package/dist/template/.pi/skills/v0/SKILL.md +264 -0
  29. package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
  30. package/dist/template/.pi/templates/DESIGN.md +76 -0
  31. package/dist/template/.pi/workflows/INDEX.md +2 -1
  32. package/dist/template/.pi/workflows/frontend-feature-workflow.md +343 -0
  33. package/dist/template/.pi/workflows/quality-loop.md +1 -1
  34. package/package.json +1 -1
  35. package/dist/template/.pi/prompts/loop-check.md +0 -87
  36. package/dist/template/.pi/prompts/loop-init.md +0 -157
  37. package/dist/template/.pi/prompts/loop-review.md +0 -90
  38. package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
  39. package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
  40. package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
  41. package/dist/template/.pi/templates/loop-github-action.yml +0 -162
  42. package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
  43. package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
  44. package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
  45. package/dist/template/.pi/templates/loop-state.json +0 -24
  46. package/dist/template/.pi/templates/loop-state.md +0 -98
  47. package/dist/template/.pi/templates/loop-vision.md +0 -110
  48. /package/dist/template/.pi/templates/{design.md → feature-design.md} +0 -0
@@ -7,11 +7,16 @@ description: MUST load when writing, reviewing, or refactoring React/Next.js cod
7
7
 
8
8
  ## When to Use
9
9
 
10
- - Applying performance guidelines to React/Next.js components or pages.
10
+ - Applying performance guidelines to React 19 / Next.js 16 components or pages.
11
+ - Optimizing Server Components, data fetching, bundle size, and re-renders.
11
12
 
12
13
  ## When NOT to Use
13
14
 
14
15
  - Non-React codebases or UI-free/backend-only changes.
16
+ - Server Actions and form patterns (use `react-server-actions` skill)
17
+ - App Router architecture (use `nextjs-app-router` skill)
18
+ - Next.js 16 caching (use `nextjs-cache` skill)
19
+ - State management (use `tanstack-query`, `zustand`, or `react-hook-form` skills)
15
20
 
16
21
 
17
22
  ## When to Apply
@@ -108,6 +113,79 @@ Reference these guidelines when:
108
113
  - `advanced-event-handler-refs` - Store event handlers in refs
109
114
  - `advanced-use-latest` - useLatest for stable callback refs
110
115
 
116
+ ## React 19 Patterns
117
+
118
+ ### Server Components: Default Data Flow
119
+
120
+ React 19 + Next.js App Router defaults to **Server Components**. Keep data fetching on the server:
121
+
122
+ ```tsx
123
+ // ✅ Server Component — fetch data where it lives
124
+ // app/posts/page.tsx
125
+ export default async function PostsPage() {
126
+ const posts = await db.post.findMany() // Direct DB access
127
+ const config = await fetchConfig() // Private API calls (no CORS)
128
+
129
+ return <PostsList posts={posts} config={config} />
130
+ }
131
+ ```
132
+
133
+ Only add `'use client'` at the interactive leaves — push it as deep as possible.
134
+
135
+ ### New Hooks Performance Impact
136
+
137
+ | Hook | Use Case | Performance Note |
138
+ |------|----------|-----------------|
139
+ | `useOptimistic` | Instant UI feedback | Replaces manual `useState` + revert logic |
140
+ | `useActionState` | Form submissions | Replaces `useFormState` (deprecated) |
141
+ | `useFormStatus` | Pending states | Read from child, not form component |
142
+ | `use()` | Unwrap promises in render | Only in Client Components — Server Components use `await` |
143
+
144
+ ### React Compiler Awareness
145
+
146
+ The React Compiler (stable, React 19+) auto-memoizes components and hooks. With compiler enabled:
147
+
148
+ - **Remove** manual `useMemo`, `useCallback`, `memo()` unless truly expensive
149
+ - **Keep** `useRef` (semantic, not memoization)
150
+ - **Keep** `useEffect` for synchronization (compiler doesn't touch effects)
151
+ - See `react-compiler` skill for full migration guide
152
+
153
+ ### `use()` for Client Component Data
154
+
155
+ ```tsx
156
+ 'use client'
157
+
158
+ import { use } from 'react'
159
+
160
+ function UserProfile({ userPromise }) {
161
+ const user = use(userPromise) // Unwrap promise in render
162
+ return <div>{user.name}</div>
163
+ }
164
+ ```
165
+
166
+ `use()` reads Promises and Context in render without a hook wrapper.
167
+
168
+ ## Next.js 16 Caching
169
+
170
+ Next.js 16 reversed the v15 caching model — everything is dynamic by default. To cache:
171
+
172
+ ```tsx
173
+ // Cached — uses `use cache` directive
174
+ export async function getPosts() {
175
+ 'use cache'
176
+ cacheLife('hours')
177
+ cacheTag('posts')
178
+ return db.post.findMany()
179
+ }
180
+
181
+ // Dynamic — no directive (default)
182
+ export async function getUserSession() {
183
+ return auth() // Always fresh
184
+ }
185
+ ```
186
+
187
+ **Migration**: Remove `export const dynamic = 'force-dynamic'` (now default). Replace `export const revalidate = 3600` with `'use cache'` + `cacheLife`. See `nextjs-cache` skill for detailed migration.
188
+
111
189
  ## How to Use
112
190
 
113
191
  Read individual rule files for detailed explanations and code examples:
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: react-compiler
3
+ description: Use when enabling, debugging, or optimizing with the React Compiler. Covers what it auto-memoizes, what it can't optimize, ESLint plugin, migration guide, and React DevTools debugging. MUST load before enabling the compiler or diagnosing memoization issues.
4
+ ---
5
+
6
+ # React Compiler (React 19+)
7
+
8
+ ## When to Use
9
+
10
+ - Enabling the React Compiler in a project
11
+ - Understanding what the compiler does and doesn't optimize
12
+ - Debugging why a component didn't get memoized
13
+ - Diagnosing unexpected re-renders in a compiler-enabled project
14
+ - Deciding between manual `useMemo`/`useCallback` vs letting the compiler handle it
15
+
16
+ ## When NOT to Use
17
+
18
+ - Non-React projects
19
+ - Debugging runtime rendering issues unrelated to memoization
20
+ - Choosing component architecture (use `react-best-practices` or `deep-module-design`)
21
+
22
+ ## What the React Compiler Does
23
+
24
+ The React Compiler **auto-memoizes** components and hooks at build time. It eliminates the need for manual `useMemo`, `useCallback`, and `memo()` in most cases.
25
+
26
+ ```tsx
27
+ // Before compiler — manual memoization
28
+ function ExpensiveList({ items }: { items: Item[] }) {
29
+ const filtered = useMemo(
30
+ () => items.filter(i => i.active),
31
+ [items]
32
+ )
33
+
34
+ const handleClick = useCallback((id: string) => {
35
+ onSelect(id)
36
+ }, [onSelect])
37
+
38
+ return filtered.map(item => (
39
+ <Item key={item.id} item={item} onClick={handleClick} />
40
+ ))
41
+ }
42
+
43
+ // With compiler — writes itself
44
+ function ExpensiveList({ items }: { items: Item[] }) {
45
+ const filtered = items.filter(i => i.active)
46
+
47
+ return filtered.map(item => (
48
+ <Item key={item.id} item={item} onClick={() => onSelect(item.id)} />
49
+ ))
50
+ }
51
+ ```
52
+
53
+ ## What the Compiler CAN Optimize
54
+
55
+ | Pattern | How |
56
+ |---------|-----|
57
+ | Components | Auto-wraps with `memo()` equivalent |
58
+ | Props & dependencies | Auto-generates dependency arrays for `useMemo`, `useCallback`, `useEffect` |
59
+ | Inline functions | Auto-memoizes `() => {}` passed as props |
60
+ | Computed values | Auto-wraps expensive computations in `useMemo` |
61
+ | Context reads | Tracks granular context reads — only re-renders when specific field changes |
62
+ | useRef stability | Stabilizes `useRef` references |
63
+
64
+ ## What the Compiler CANNOT Optimize
65
+
66
+ | Limitation | What to Do |
67
+ |-----------|------------|
68
+ | **Side effects in render** | Fix — compiler bails out on impure renders |
69
+ | **Mutating state directly** | Fix — use setState functions |
70
+ | **Dynamic `key` values from `Math.random()`** | Fix — use stable keys |
71
+ | **Third-party libraries that break Rules of React** | Wait for library updates |
72
+ | **Components with `ref` + mutating `ref.current` in render** | Move mutation to event handler or effect |
73
+ | **Deeply nested context that's read broadly** | Split context or use selectors (Zustand) |
74
+ | **Extremely large dependency arrays (100+ items)** | Restructure: split component or function |
75
+ | **Code using `eval()` or `new Function()`** | Remove dynamic code evaluation |
76
+
77
+ ## Enabling the Compiler
78
+
79
+ ### Next.js (15+)
80
+
81
+ ```bash
82
+ npm install babel-plugin-react-compiler
83
+ ```
84
+
85
+ ```ts
86
+ // next.config.ts
87
+ import type { NextConfig } from 'next'
88
+
89
+ const nextConfig: NextConfig = {
90
+ experimental: {
91
+ reactCompiler: true,
92
+ },
93
+ }
94
+ ```
95
+
96
+ ### Vite (React)
97
+
98
+ ```bash
99
+ npm install babel-plugin-react-compiler
100
+ ```
101
+
102
+ ```ts
103
+ // vite.config.ts
104
+ import { defineConfig } from 'vite'
105
+ import react from '@vitejs/plugin-react'
106
+
107
+ export default defineConfig({
108
+ plugins: [
109
+ react({
110
+ babel: {
111
+ plugins: [['babel-plugin-react-compiler', { target: '19' }]],
112
+ },
113
+ }),
114
+ ],
115
+ })
116
+ ```
117
+
118
+ ## ESLint Plugin
119
+
120
+ ```bash
121
+ npm install eslint-plugin-react-compiler
122
+ ```
123
+
124
+ ```json
125
+ // .eslintrc.json
126
+ {
127
+ "plugins": ["react-compiler"],
128
+ "rules": {
129
+ "react-compiler/react-compiler": "error"
130
+ }
131
+ }
132
+ ```
133
+
134
+ The ESLint plugin detects **Rules of React violations** that would cause the compiler to bail out — catches issues at lint time before they become runtime problems.
135
+
136
+ ## React DevTools Integration
137
+
138
+ With the compiler enabled, React DevTools shows:
139
+
140
+ - **"Memo ✨"** badge on auto-memoized components
141
+ - Components that **failed** to compile — shown with a warning badge
142
+ - Why it failed: hover the badge for explanation
143
+
144
+ Open React DevTools → Components tab → look for ✨ beside component names.
145
+
146
+ ## Migration Guide
147
+
148
+ ### Phase 1: ESLint First (No Compiler)
149
+
150
+ ```bash
151
+ npm install eslint-plugin-react-compiler
152
+ ```
153
+
154
+ Enable only the ESLint rule. Fix all violations. This ensures your code follows Rules of React — a prerequisite for the compiler.
155
+
156
+ ### Phase 2: Enable Compiler on CI
157
+
158
+ ```ts
159
+ // next.config.ts — enable on CI/staging first
160
+ reactCompiler: process.env.CI === 'true'
161
+ ```
162
+
163
+ Run your test suite. Check for behavioral changes.
164
+
165
+ ### Phase 3: Remove Manual Memoization
166
+
167
+ Once the compiler is stable in production:
168
+
169
+ ```tsx
170
+ // Remove these — compiler handles them:
171
+ // - useMemo for computed values
172
+ // - useCallback for event handlers
173
+ // - React.memo() wrapping
174
+
175
+ // Keep these — they serve semantic purposes:
176
+ // - useMemo for expensive computations the compiler can't prove
177
+ // - useRef for mutable values
178
+ // - useEffect for synchronization
179
+ ```
180
+
181
+ ### Phase 4: Full Production Enable
182
+
183
+ ```ts
184
+ reactCompiler: true
185
+ ```
186
+
187
+ ## Gradual Adoption
188
+
189
+ Enable per-directory via compiler directive comments:
190
+
191
+ ```tsx
192
+ // Opt-in specific files:
193
+ 'use memo'
194
+
195
+ // Opt-out specific files:
196
+ 'use no memo'
197
+ ```
198
+
199
+ Use `'use memo'` at the top of a file to enable the compiler for that file only, even if the project-level config is off.
200
+
201
+ ## When to Keep Manual Memoization
202
+
203
+ Even with the compiler, keep manual memoization for:
204
+
205
+ - **Truly expensive computations**: The compiler is conservative — if you know something is heavy, `useMemo` makes it explicit
206
+ - **Custom hooks with object returns**: `useMemo` on the return value ensures referential stability
207
+ - **Third-party integration**: When passing objects to non-React libraries that do shallow comparisons
208
+
209
+ ## Common Pitfalls
210
+
211
+ | Pitfall | Fix |
212
+ |---------|-----|
213
+ | Side effects during render (console.log, date, random) | Move to event handlers or effects |
214
+ | Mutating props or state in render | Use immutable updates |
215
+ | Assuming compiler fixes all performance | Compiler handles memoization only — waterfalls, bundle size, SSR still need attention |
216
+ | Leaving manual memoization everywhere | Remove redundant `useMemo`/`useCallback` — they add noise with no benefit |
217
+ | Not running ESLint plugin before enabling | Run ESLint first and fix all violations — this is the #1 source of compiler bailouts |
218
+ | `useMemo` with empty dependency for object identity | Compiler handles this — remove unless it's truly expensive |
219
+
220
+ ## Integration with Other Skills
221
+
222
+ | Skill | How Compiler Changes It |
223
+ |-------|------------------------|
224
+ | `react-best-practices` | `rerender-memo`, `rerender-functional-setstate` become unnecessary with compiler |
225
+ | `react-server-actions` | No change — Server Actions don't use compiler |
226
+ | `zustand` | Selective subscriptions still recommended — compiler doesn't replace selectors |
227
+ | `tanstack-query` | No change — data fetching patterns unchanged |
228
+
229
+ ## Verification
230
+
231
+ - [ ] ESLint plugin enabled and passing with zero violations
232
+ - [ ] Compiler enabled in `next.config.ts` or `vite.config.ts`
233
+ - [ ] React DevTools shows ✨ on auto-memoized components
234
+ - [ ] No unexpected re-renders after enabling (profile with DevTools)
235
+ - [ ] Test suite passes with compiler enabled
236
+ - [ ] Manual `useMemo`/`useCallback` removed where compiler handles them
237
+ - [ ] No side effects in render (build warnings if any)
@@ -0,0 +1,374 @@
1
+ ---
2
+ name: react-hook-form
3
+ description: Use when building forms with React Hook Form v7 and Zod v3. Covers useForm, controlled vs uncontrolled, zodResolver, conditional fields, field arrays, Server Actions integration. MUST load before any form implementation.
4
+ ---
5
+
6
+ # React Hook Form + Zod
7
+
8
+ ## When to Use
9
+
10
+ - Building complex forms with many fields and validation rules
11
+ - Integrating Zod schemas for type-safe form validation
12
+ - Handling conditional fields and dynamic field arrays
13
+ - Optimizing form performance (uncontrolled inputs, minimal re-renders)
14
+ - Integrating forms with Server Actions in Next.js
15
+ - Field-level and form-level validation with custom error messages
16
+
17
+ ## When NOT to Use
18
+
19
+ - Simple forms with 1-2 fields (use plain Server Actions)
20
+ - Read-only data display (no form needed)
21
+ - Forms that must work without JavaScript (use progressive enhancement Server Actions)
22
+
23
+ ## Setup
24
+
25
+ ```bash
26
+ npm install react-hook-form @hookform/resolvers zod
27
+ ```
28
+
29
+ ## Basic Form
30
+
31
+ ```tsx
32
+ 'use client'
33
+
34
+ import { useForm } from 'react-hook-form'
35
+ import { zodResolver } from '@hookform/resolvers/zod'
36
+ import { z } from 'zod'
37
+
38
+ const schema = z.object({
39
+ name: z.string().min(2, 'Name must be at least 2 characters'),
40
+ email: z.string().email('Invalid email address'),
41
+ age: z.coerce.number().min(18, 'Must be 18 or older'),
42
+ })
43
+
44
+ type FormData = z.infer<typeof schema>
45
+
46
+ export function SignupForm() {
47
+ const {
48
+ register,
49
+ handleSubmit,
50
+ formState: { errors, isSubmitting },
51
+ } = useForm<FormData>({
52
+ resolver: zodResolver(schema),
53
+ defaultValues: { name: '', email: '', age: 0 },
54
+ })
55
+
56
+ const onSubmit = async (data: FormData) => {
57
+ await createUser(data) // Server Action or API call
58
+ }
59
+
60
+ return (
61
+ <form onSubmit={handleSubmit(onSubmit)}>
62
+ <div>
63
+ <input {...register('name')} placeholder="Name" />
64
+ {errors.name && <p className="text-red-500">{errors.name.message}</p>}
65
+ </div>
66
+
67
+ <div>
68
+ <input {...register('email')} placeholder="Email" />
69
+ {errors.email && <p className="text-red-500">{errors.email.message}</p>}
70
+ </div>
71
+
72
+ <div>
73
+ <input type="number" {...register('age')} placeholder="Age" />
74
+ {errors.age && <p className="text-red-500">{errors.age.message}</p>}
75
+ </div>
76
+
77
+ <button type="submit" disabled={isSubmitting}>
78
+ {isSubmitting ? 'Submitting...' : 'Sign Up'}
79
+ </button>
80
+ </form>
81
+ )
82
+ }
83
+ ```
84
+
85
+ ## Controlled Components (shadcn/ui + Zod)
86
+
87
+ React Hook Form is uncontrolled by default. Use `Controller` for controlled UI libraries:
88
+
89
+ ```tsx
90
+ import { useForm, Controller } from 'react-hook-form'
91
+ import { zodResolver } from '@hookform/resolvers/zod'
92
+ import {
93
+ Select,
94
+ SelectContent,
95
+ SelectItem,
96
+ SelectTrigger,
97
+ SelectValue,
98
+ } from '@/components/ui/select'
99
+
100
+ const schema = z.object({
101
+ plan: z.enum(['free', 'pro', 'enterprise']),
102
+ })
103
+
104
+ export function PlanForm() {
105
+ const { control, handleSubmit } = useForm({
106
+ resolver: zodResolver(schema),
107
+ })
108
+
109
+ return (
110
+ <form onSubmit={handleSubmit(onSubmit)}>
111
+ <Controller
112
+ name="plan"
113
+ control={control}
114
+ render={({ field }) => (
115
+ <Select onValueChange={field.onChange} value={field.value}>
116
+ <SelectTrigger>
117
+ <SelectValue placeholder="Select a plan" />
118
+ </SelectTrigger>
119
+ <SelectContent>
120
+ <SelectItem value="free">Free</SelectItem>
121
+ <SelectItem value="pro">Pro</SelectItem>
122
+ <SelectItem value="enterprise">Enterprise</SelectItem>
123
+ </SelectContent>
124
+ </Select>
125
+ )}
126
+ />
127
+ </form>
128
+ )
129
+ }
130
+ ```
131
+
132
+ ## Zod Schema Patterns
133
+
134
+ ```tsx
135
+ // Refinement — cross-field validation
136
+ const schema = z.object({
137
+ password: z.string().min(8),
138
+ confirmPassword: z.string(),
139
+ }).refine((data) => data.password === data.confirmPassword, {
140
+ message: "Passwords don't match",
141
+ path: ['confirmPassword'], // Attach error to confirmPassword field
142
+ })
143
+
144
+ // SuperRefine — complex logic
145
+ const schema = z.object({
146
+ email: z.string().email(),
147
+ username: z.string().min(3),
148
+ }).superRefine((data, ctx) => {
149
+ if (data.email === data.username) {
150
+ ctx.addIssue({
151
+ code: z.ZodIssueCode.custom,
152
+ message: 'Email and username must be different',
153
+ path: ['username'],
154
+ })
155
+ }
156
+ })
157
+
158
+ // Coercion — convert string inputs
159
+ z.coerce.number() // "42" → 42
160
+ z.coerce.boolean() // "true" → true
161
+ z.coerce.date() // "2024-01-01" → Date
162
+
163
+ // Preprocess — custom coercion
164
+ z.preprocess((val) => {
165
+ if (typeof val === 'string') return val.trim()
166
+ return val
167
+ }, z.string().min(1))
168
+ ```
169
+
170
+ ## Conditional Fields
171
+
172
+ ```tsx
173
+ const schema = z.discriminatedUnion('accountType', [
174
+ z.object({
175
+ accountType: z.literal('personal'),
176
+ name: z.string().min(2),
177
+ }),
178
+ z.object({
179
+ accountType: z.literal('business'),
180
+ companyName: z.string().min(2),
181
+ vatNumber: z.string().regex(/^[A-Z]{2}\d{8,12}$/),
182
+ }),
183
+ ])
184
+
185
+ type FormData = z.infer<typeof schema>
186
+
187
+ export function AccountForm() {
188
+ const { register, watch, handleSubmit } = useForm<FormData>({
189
+ resolver: zodResolver(schema),
190
+ })
191
+
192
+ const accountType = watch('accountType')
193
+
194
+ return (
195
+ <form onSubmit={handleSubmit(onSubmit)}>
196
+ <select {...register('accountType')}>
197
+ <option value="personal">Personal</option>
198
+ <option value="business">Business</option>
199
+ </select>
200
+
201
+ {accountType === 'personal' && (
202
+ <input {...register('name')} />
203
+ )}
204
+ {accountType === 'business' && (
205
+ <>
206
+ <input {...register('companyName')} />
207
+ <input {...register('vatNumber')} />
208
+ </>
209
+ )}
210
+ </form>
211
+ )
212
+ }
213
+ ```
214
+
215
+ ## Field Arrays (Dynamic Fields)
216
+
217
+ ```tsx
218
+ import { useFieldArray } from 'react-hook-form'
219
+
220
+ const schema = z.object({
221
+ emails: z.array(
222
+ z.object({ value: z.string().email() })
223
+ ).min(1, 'At least one email required'),
224
+ })
225
+
226
+ export function EmailListForm() {
227
+ const { register, control, handleSubmit } = useForm({
228
+ resolver: zodResolver(schema),
229
+ defaultValues: { emails: [{ value: '' }] },
230
+ })
231
+
232
+ const { fields, append, remove } = useFieldArray({
233
+ control,
234
+ name: 'emails',
235
+ })
236
+
237
+ return (
238
+ <form onSubmit={handleSubmit(onSubmit)}>
239
+ {fields.map((field, index) => (
240
+ <div key={field.id}>
241
+ <input {...register(`emails.${index}.value`)} placeholder="Email" />
242
+ <button type="button" onClick={() => remove(index)}>Remove</button>
243
+ </div>
244
+ ))}
245
+ <button type="button" onClick={() => append({ value: '' })}>
246
+ Add Email
247
+ </button>
248
+ </form>
249
+ )
250
+ }
251
+ ```
252
+
253
+ ## Integration with Server Actions
254
+
255
+ React Hook Form can delegate submission to a Server Action:
256
+
257
+ ```tsx
258
+ 'use client'
259
+
260
+ import { useForm } from 'react-hook-form'
261
+ import { zodResolver } from '@hookform/resolvers/zod'
262
+ import { createUser } from './actions'
263
+ import { useActionState } from 'react'
264
+
265
+ const schema = z.object({
266
+ name: z.string().min(2),
267
+ email: z.string().email(),
268
+ })
269
+
270
+ export function UserForm() {
271
+ const [serverState, formAction] = useActionState(createUser, null)
272
+
273
+ const { register, formState: { errors } } = useForm({
274
+ resolver: zodResolver(schema),
275
+ })
276
+
277
+ return (
278
+ <form action={formAction}>
279
+ <input {...register('name')} />
280
+ {errors.name?.message || serverState?.errors?.name?.[0]}
281
+
282
+ <input {...register('email')} />
283
+ {errors.email?.message || serverState?.errors?.email?.[0]}
284
+
285
+ <button type="submit">Create</button>
286
+ </form>
287
+ )
288
+ }
289
+ ```
290
+
291
+ **Decision guide:**
292
+ - **Plain Server Actions** → Simple forms, progressive enhancement needed
293
+ - **React Hook Form** → Complex forms, dynamic fields, client-side validation UX
294
+ - **Combined** → RHF for client UX + Server Action for submission
295
+
296
+ ## Form State Reference
297
+
298
+ ```tsx
299
+ const { formState } = useForm()
300
+
301
+ formState.isDirty // User modified any field
302
+ formState.isValid // All fields pass validation
303
+ formState.isSubmitting // Currently submitting
304
+ formState.isSubmitted // Form was submitted at least once
305
+ formState.isSubmitSuccessful // Last submit succeeded
306
+ formState.errors // Field-level errors object
307
+ formState.dirtyFields // Which fields were modified
308
+ formState.touchedFields // Which fields gained and lost focus
309
+ ```
310
+
311
+ ## Performance: `useForm` Options
312
+
313
+ ```tsx
314
+ const { register } = useForm({
315
+ mode: 'onBlur', // Validate on blur (default: onSubmit)
316
+ reValidateMode: 'onChange', // Re-validate after first submit
317
+ shouldFocusError: true, // Focus first field with error after submit
318
+ criteriaMode: 'all', // Show all validation errors per field
319
+ delayError: 500, // Delay error display (ms) for async validation
320
+ })
321
+ ```
322
+
323
+ ## Debounced Validation (Async)
324
+
325
+ ```tsx
326
+ const schema = z.object({
327
+ username: z.string().min(3).refine(
328
+ async (username) => {
329
+ const available = await checkUsername(username)
330
+ return available
331
+ },
332
+ { message: 'Username is already taken' }
333
+ ),
334
+ })
335
+
336
+ // React Hook Form debounces the refine call
337
+ // Add throttle via watch + useEffect if needed
338
+ ```
339
+
340
+ ## Common Pitfalls
341
+
342
+ | Pitfall | Fix |
343
+ |---------|-----|
344
+ | Mixing `register` and `Controller` for same field | Pick one — use `Controller` for UI libraries, `register` for native inputs |
345
+ | Forgetting `defaultValues` shape | `defaultValues` must match schema shape — otherwise fields are undefined |
346
+ | `watch` in render causing loops | Use `watch` sparingly; prefer `getValues()` in callbacks |
347
+ | `setValue` without `shouldDirty`/`shouldValidate` | `setValue('field', val, { shouldDirty: true, shouldValidate: true })` |
348
+ | Zod `refine` on field that doesn't exist yet | Use `superRefine` and `addIssue` with explicit `path` |
349
+ | Not forwarding `ref` in custom components | Use `React.forwardRef` or Controller for custom inputs |
350
+ | `handleSubmit` not wrapping async handler | Always `async (data) => { await ... }` — unhandled promise rejections crash |
351
+ | `useFieldArray` `key` using index | Always use `field.id` as key (not index) — stable across add/remove |
352
+
353
+ ## When to Use React Hook Form vs Server Actions Only
354
+
355
+ | React Hook Form | Server Actions Only |
356
+ |-----------------|-------------------|
357
+ | Complex validation UX (real-time errors) | Simple forms (2-3 fields) |
358
+ | Dynamic field arrays | Progressive enhancement required |
359
+ | Conditional fields that affect validation | No client-side validation needed |
360
+ | Multi-step wizards | Static forms that rarely change |
361
+ | shadcn Select/DatePicker/Combobox | Native inputs only |
362
+ | Field-level async validation (username check) | Server-only validation |
363
+
364
+ ## Verification
365
+
366
+ - [ ] `zodResolver(schema)` configured — links Zod to RHF
367
+ - [ ] `defaultValues` match the schema structure
368
+ - [ ] All controlled components use `Controller` or `useController`
369
+ - [ ] `useFieldArray` keys use `field.id` (not index)
370
+ - [ ] Conditional fields use `watch` with `z.discriminatedUnion` or `z.union`
371
+ - [ ] `handleSubmit` wraps async function
372
+ - [ ] Field-level errors displayed via `formState.errors`
373
+ - [ ] `isSubmitting` disables submit button during submission
374
+ - [ ] Cross-field validation uses `.refine()` or `.superRefine()` with `path`