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.
Builds performant React forms with validation, error handling, and Zod schema integration.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Performant, flexible forms with easy validation and minimal re-renders.
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>
);
}
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,
});
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;
<input {...register('firstName')} />
<input {...register('lastName')} />
<input type="email" {...register('email')} />
<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',
},
})}
/>
<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';
},
})}
/>
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>
);
}
npm install @hookform/error-message
import { ErrorMessage } from '@hookform/error-message';
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => <p className="error">{message}</p>}
/>
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>
);
}
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>
);
}
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>
);
}
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
const { trigger } = useForm<FormData>();
// Validate single field
await trigger('email');
// Validate multiple fields
await trigger(['email', 'password']);
// Validate all fields
await trigger();
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>
);
}
// 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>
);
}
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>
);
}
| 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 |
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.