npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill form-reactSKILL.md
Form React
Production React form patterns. Default stack: React Hook Form + Zod.
Quick Start
npm install react-hook-form @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. Define schema
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters')
});
type FormData = z.infer<typeof schema>;
// 2. Use form
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onBlur' // Reward early, punish late
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<input {...register('email')} type="email" autoComplete="email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('password')} type="password" autoComplete="current-password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Sign in</button>
</form>
);
}
When to Use Which
| Criteria | React Hook Form | TanStack Form |
|---|---|---|
| Performance | ✅ Best (uncontrolled) | Good (controlled) |
| Bundle size | 12KB | ~15KB |
| TypeScript | Good | ✅ Excellent |
| Cross-framework | ❌ React only | ✅ Multi-framework |
| React Native | Requires workarounds | ✅ Native support |
| Built-in async validation | Manual | ✅ Built-in debouncing |
| Ecosystem | ✅ Mature (4+ years) | Growing |
Default: React Hook Form — Better performance for most React web apps.
Use TanStack Form when:
- Building cross-framework component libraries
- Need strict controlled component behavior
- Heavy async validation (username checks)
- React Native applications
React Hook Form Patterns
Basic Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, type LoginFormData } from './schemas';
export function LoginForm({ onSubmit }: { onSubmit: (data: LoginFormData) => void }) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, touchedFields }
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
mode: 'onBlur', // First validation on blur (punish late)
reValidateMode: 'onChange' // Re-validate on change (real-time correction)
});
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div className="form-field">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
autoComplete="email"
aria-invalid={!!errors.email}
{...register('email')}
/>
{touchedFields.email && errors.email && (
<span role="alert">{errors.email.message}</span>
)}
</div>
<div className="form-field">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
autoComplete="current-password"
aria-invalid={!!errors.password}
{...register('password')}
/>
{touchedFields.password && errors.password && (
<span role="alert">{errors.password.message}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign in'}
</button>
</form>
);
}
Reusable Form Field Component
// FormField.tsx
import { useFormContext } from 'react-hook-form';
import { ReactNode } from 'react';
interface FormFieldProps {
name: string;
label: string;
type?: string;
autoComplete?: string;
hint?: string;
required?: boolean;
children?: ReactNode;
}
export function FormField({
name,
label,
type = 'text',
autoComplete,
hint,
required,
children
}: FormFieldProps) {
const {
register,
formState: { errors, touchedFields }
} = useFormContext();
const error = errors[name];
const touched = touchedFields[name];
const showError = touched && error;
const showValid = touched && !error;
return (
<div className={`form-field ${showError ? 'error' : ''} ${showValid ? 'valid' : ''}`}>
<label htmlFor={name}>
{label}
{required && <span aria-hidden="true">*</span>}
</label>
{hint && <span className="hint">{hint}</span>}
{children || (
<input
id={name}
type={type}
autoComplete={autoComplete}
aria-invalid={!!error}
aria-describedby={error ? `${name}-error` : undefined}
{...register(name)}
/>
)}
{showError && (
<span id={`${name}-error`} role="alert" className="error-message">
{error.message as string}
</span>
)}
</div>
);
}
Using FormProvider for Nested Components
// F
...
Repository Stats
Stars4
Forks0