← Back to list

react-hook-form
by mgd34msu
Plug in, Receive good vibes.
⭐ 2🍴 0📅 Jan 25, 2026
SKILL.md
name: react-hook-form description: Builds performant forms with React Hook Form including validation, error handling, and schema integration. Use when creating forms, validating inputs, integrating with Zod, or handling complex form state.
React Hook Form
Performant, flexible forms with easy validation and minimal re-renders.
Quick Start
Install:
npm install react-hook-form
Basic form:
import { useForm } from 'react-hook-form';
interface FormData {
email: string;
password: string;
}
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: 'Email is required' })} />
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', { required: 'Password is required' })}
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
useForm Hook
Options
const {
register,
handleSubmit,
watch,
formState,
reset,
setValue,
getValues,
trigger,
control,
} = useForm<FormData>({
defaultValues: {
email: '',
password: '',
},
mode: 'onBlur', // 'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all'
reValidateMode: 'onChange',
criteriaMode: 'firstError', // 'all' for all errors
shouldFocusError: true,
});
Form State
const {
errors, // Validation errors
isDirty, // Form has been modified
isValid, // All validations pass
isSubmitting, // Form is submitting
isSubmitted, // Form has been submitted
isSubmitSuccessful,
submitCount, // Number of submissions
dirtyFields, // Modified fields
touchedFields, // Touched fields
} = formState;
Register
Basic Registration
<input {...register('firstName')} />
<input {...register('lastName')} />
<input type="email" {...register('email')} />
With Validation
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
/>
<input
{...register('age', {
required: 'Age is required',
min: { value: 18, message: 'Must be at least 18' },
max: { value: 100, message: 'Must be under 100' },
})}
/>
<input
{...register('username', {
required: 'Username is required',
minLength: { value: 3, message: 'At least 3 characters' },
maxLength: { value: 20, message: 'At most 20 characters' },
})}
/>
<input
{...register('password', {
required: 'Password is required',
validate: {
hasUppercase: (value) =>
/[A-Z]/.test(value) || 'Must contain uppercase',
hasNumber: (value) =>
/[0-9]/.test(value) || 'Must contain number',
},
})}
/>
Async Validation
<input
{...register('username', {
validate: async (value) => {
const response = await fetch(`/api/check-username?name=${value}`);
const { available } = await response.json();
return available || 'Username is taken';
},
})}
/>
Error Handling
Display Errors
function Form() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input
{...register('email', { required: 'Email is required' })}
className={errors.email ? 'error' : ''}
/>
{errors.email && (
<span className="error-message">{errors.email.message}</span>
)}
</div>
</form>
);
}
ErrorMessage Component
npm install @hookform/error-message
import { ErrorMessage } from '@hookform/error-message';
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => <p className="error">{message}</p>}
/>
Zod Integration
Install:
npm install @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z
.string()
.min(8, 'At least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});
type FormData = z.infer<typeof schema>;
function SignUpForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
email: '',
password: '',
confirmPassword: '',
},
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<input type="password" {...register('confirmPassword')} />
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<button type="submit">Sign Up</button>
</form>
);
}
Controller
For controlled components (MUI, Radix, custom inputs):
import { useForm, Controller } from 'react-hook-form';
import { TextField, Select, MenuItem } from '@mui/material';
function ControlledForm() {
const { control, handleSubmit } = useForm<FormData>();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
rules={{ required: 'Email is required' }}
render={({ field, fieldState: { error } }) => (
<TextField
{...field}
label="Email"
error={!!error}
helperText={error?.message}
/>
)}
/>
<Controller
name="role"
control={control}
defaultValue=""
render={({ field }) => (
<Select {...field} label="Role">
<MenuItem value="admin">Admin</MenuItem>
<MenuItem value="user">User</MenuItem>
</Select>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
Watch
function WatchExample() {
const { register, watch } = useForm<FormData>();
// Watch single field
const email = watch('email');
// Watch multiple fields
const [firstName, lastName] = watch(['firstName', 'lastName']);
// Watch all fields
const allFields = watch();
// Watch with callback
useEffect(() => {
const subscription = watch((value, { name, type }) => {
console.log(value, name, type);
});
return () => subscription.unsubscribe();
}, [watch]);
return (
<form>
<input {...register('email')} />
<p>Current email: {email}</p>
</form>
);
}
Form Actions
Set Values
const { setValue, reset, getValues } = useForm<FormData>();
// Set single value
setValue('email', 'test@example.com');
// Set with options
setValue('email', 'test@example.com', {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
// Get values
const email = getValues('email');
const allValues = getValues();
// Reset form
reset(); // Reset to defaultValues
reset({ email: 'new@example.com' }); // Reset with new values
Trigger Validation
const { trigger } = useForm<FormData>();
// Validate single field
await trigger('email');
// Validate multiple fields
await trigger(['email', 'password']);
// Validate all fields
await trigger();
Field Arrays
npm install react-hook-form
import { useForm, useFieldArray } from 'react-hook-form';
interface FormData {
users: { name: string; email: string }[];
}
function DynamicForm() {
const { register, control, handleSubmit } = useForm<FormData>({
defaultValues: {
users: [{ name: '', email: '' }],
},
});
const { fields, append, remove, move } = useFieldArray({
control,
name: 'users',
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`users.${index}.name`)} placeholder="Name" />
<input {...register(`users.${index}.email`)} placeholder="Email" />
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', email: '' })}>
Add User
</button>
<button type="submit">Submit</button>
</form>
);
}
Form with Server Action
// Next.js App Router
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useTransition } from 'react';
import { submitForm } from './actions';
function ContactForm() {
const [isPending, startTransition] = useTransition();
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
startTransition(async () => {
const result = await submitForm(data);
if (result.success) {
reset();
}
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} disabled={isPending} />
<input {...register('email')} disabled={isPending} />
<textarea {...register('message')} disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send'}
</button>
</form>
);
}
With shadcn/ui Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
function ProfileForm() {
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
username: '',
email: '',
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Best Practices
- Use Zod for validation - Type-safe, reusable schemas
- Set defaultValues - Prevents undefined values
- Use Controller for UI libs - Proper integration
- Watch sparingly - Only when needed to avoid re-renders
- Show loading state - Disable form during submission
Common Mistakes
| Mistake | Fix |
|---|---|
| Missing name prop | Always provide unique name |
| Not using key in arrays | Use field.id for key |
| Direct mutation | Use setValue/reset |
| Validation on every keystroke | Use mode: 'onBlur' |
| Missing error display | Always show error messages |
Reference Files
- references/patterns.md - Form patterns
- references/validation.md - Validation rules
- references/integrations.md - UI library integration
Score
Total Score
60/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
✓LICENSE
ライセンスが設定されている
+10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon