@minhduydev/mdpi 0.4.1 → 0.6.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/dist/index.js +4 -2
- package/dist/template/.pi/AGENTS.md +1 -1
- package/dist/template/.pi/README.md +2 -3
- package/dist/template/.pi/VERSION +1 -1
- package/dist/template/.pi/agents/explore.md +1 -1
- package/dist/template/.pi/agents/scout.md +1 -1
- package/dist/template/.pi/extensions/templates-injector.ts +35 -7
- package/dist/template/.pi/prompts/INDEX.md +3 -9
- package/dist/template/.pi/prompts/gc.md +2 -1
- package/dist/template/.pi/prompts/verify.md +24 -0
- package/dist/template/.pi/skills/INDEX.md +40 -8
- package/dist/template/.pi/skills/dcp-hygiene/SKILL.md +1 -1
- package/dist/template/.pi/skills/frontend-design/SKILL.md +1 -1
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-advanced.md +88 -15
- package/dist/template/.pi/skills/frontend-design/references/animation/motion-core.md +148 -13
- package/dist/template/.pi/skills/frontend-design/references/shadcn/setup.md +127 -20
- package/dist/template/.pi/skills/nextjs-app-router/SKILL.md +334 -0
- package/dist/template/.pi/skills/nextjs-cache/SKILL.md +262 -0
- package/dist/template/.pi/skills/react-best-practices/SKILL.md +79 -1
- package/dist/template/.pi/skills/react-compiler/SKILL.md +237 -0
- package/dist/template/.pi/skills/react-hook-form/SKILL.md +374 -0
- package/dist/template/.pi/skills/react-server-actions/SKILL.md +299 -0
- package/dist/template/.pi/skills/shadcn-ui/SKILL.md +404 -0
- package/dist/template/.pi/skills/tanstack-query/SKILL.md +330 -0
- package/dist/template/.pi/skills/v0/SKILL.md +264 -0
- package/dist/template/.pi/skills/zustand/SKILL.md +333 -0
- package/package.json +1 -1
- package/dist/template/.pi/context/fallow.md +0 -137
- package/dist/template/.pi/prompts/loop-check.md +0 -87
- package/dist/template/.pi/prompts/loop-init.md +0 -157
- package/dist/template/.pi/prompts/loop-review.md +0 -90
- package/dist/template/.pi/skills/loop-audit/SKILL.md +0 -141
- package/dist/template/.pi/skills/loop-cost/SKILL.md +0 -130
- package/dist/template/.pi/skills/loop-engineering/SKILL.md +0 -175
- package/dist/template/.pi/templates/loop-github-action.yml +0 -162
- package/dist/template/.pi/templates/loop-orchestrator.sh +0 -514
- package/dist/template/.pi/templates/loop-orchestrator.test.ts +0 -332
- package/dist/template/.pi/templates/loop-orchestrator.ts +0 -936
- package/dist/template/.pi/templates/loop-state.json +0 -24
- package/dist/template/.pi/templates/loop-state.md +0 -98
- package/dist/template/.pi/templates/loop-vision.md +0 -110
|
@@ -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`
|