L7: Create SignInForm with React Hook Form
Build a validated form with React Hook Form and Zod
Time to build a powerful, validated form using React Hook Form and Zod! 📝
Why React Hook Form?
Traditional forms are painful:
// ❌ Manual state management (tedious)
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState('');
const [passwordError, setPasswordError] = useState('');
const [touched, setTouched] = useState({});
// Lots of boilerplate...React Hook Form is better:
// ✅ Simple, powerful
const { register, handleSubmit, formState: { errors } } = useForm();
<input {...register('email')} />
// That's it!Benefits:
- ✅ Less code: No manual state
- ✅ Better performance: Fewer re-renders
- ✅ Built-in validation: Integrated with Zod
- ✅ Error handling: Automatic
- ✅ TypeScript support: Full type safety
Why Zod?
Zod provides schema validation:
// Define what valid data looks like
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be 8+ characters'),
});
// Validate data
schema.parse({ email: 'test@test.com', password: '12345678' });
// ✅ Valid!
schema.parse({ email: 'bad-email', password: '123' });
// ❌ Throws error with messagesBenefits:
- ✅ Type safety: TypeScript infers types
- ✅ Reusable schemas: Use in forms, APIs, databases
- ✅ Clear error messages: User-friendly
- ✅ Composable: Build complex validations
Install Dependencies
Install React Hook Form and Zod:
npm install react-hook-form zod @hookform/resolversWhat each does:
react-hook-form: Form state and validationzod: Schema validation@hookform/resolvers: Connects Zod to React Hook Form
Verify installation:
npm list react-hook-form zodShould show installed versions (v7.x+ for react-hook-form, v3.x+ for zod).
Create Validation Schema
Define what valid sign-in data looks like:
import { z } from 'zod';
export const signInSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
password: z
.string()
.min(1, 'Password is required')
.min(8, 'Password must be at least 8 characters'),
});Create SignInForm Component
Build the form with validation:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { signInSchema } from '@/schemas/signInSchema';
function SignInForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(signInSchema),
});
const onSubmit = async (data) => {
console.log('Form data:', data);
// TODO: Implement sign-in logic in Lesson 9
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{/* Email Field */}
<div className="form-field">
<label htmlFor="email" className="form-label">
Email
</label>
<input
id="email"
type="email"
{...register('email')}
className={`form-input ${errors.email ? 'form-input-error' : ''}`}
placeholder="you@example.com"
/>
{errors.email && (
<p className="form-error">{errors.email.message}</p>
)}
</div>
{/* Password Field */}
<div className="form-field">
<label htmlFor="password" className="form-label">
Password
</label>
<input
id="password"
type="password"
{...register('password')}
className={`form-input ${errors.password ? 'form-input-error' : ''}`}
placeholder="••••••••"
/>
{errors.password && (
<p className="form-error">{errors.password.message}</p>
)}
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
}
export default SignInForm;Add Form Styling
Create beautiful, accessible form styles:
/* Form Container */
.sign-in-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Form Field */
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Label */
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
cursor: pointer;
}
/* Input */
.form-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
color: #1f2937;
background: white;
border: 1px solid #d1d5db;
border-radius: 8px;
transition: all 0.2s;
outline: none;
}
.form-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-input::placeholder {
color: #9ca3af;
}
/* Input Error State */
.form-input-error {
border-color: #ef4444;
}
.form-input-error:focus {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Error Message */
.form-error {
font-size: 0.875rem;
color: #ef4444;
margin: 0;
display: flex;
align-items: center;
gap: 0.25rem;
}
.form-error::before {
content: '⚠️';
font-size: 1rem;
}
/* Submit Button */
.submit-button {
width: 100%;
padding: 0.875rem 1rem;
font-size: 1rem;
font-weight: 600;
color: white;
background: #3b82f6;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.submit-button:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.submit-button:active:not(:disabled) {
transform: translateY(0);
}
.submit-button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
/* Focus Visible (Accessibility) */
.form-input:focus-visible,
.submit-button:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}Understanding the Code
The useForm hook provides everything:
const {
register, // Register inputs
handleSubmit, // Handle form submission
formState, // Form state (errors, isSubmitting, etc.)
} = useForm({
resolver: zodResolver(signInSchema), // Use Zod for validation
});Breaking down formState:
const { errors, isSubmitting } = formState;
// errors: Object with validation errors
// {
// email: { message: 'Invalid email' },
// password: { message: 'Too short' }
// }
// isSubmitting: Boolean
// true → Form is submitting
// false → Form is idleresolver connects Zod:
// Without resolver (manual validation)
useForm({
// You'd have to validate manually
});
// With resolver (automatic validation)
useForm({
resolver: zodResolver(signInSchema), // Zod handles it!
});When validation happens:
User types → onChange validation (optional)
User blurs field → onBlur validation (default)
User submits → onSubmit validation (always)Configure with mode:
useForm({
mode: 'onChange', // Validate on every change
mode: 'onBlur', // Validate when field loses focus (default)
mode: 'onSubmit', // Only validate on submit
});register connects inputs to form:
<input {...register('email')} />What {...register('email')} does:
Spreads these props onto the input:
<input
name="email"
ref={/* React Hook Form's internal ref */}
onChange={/* Internal change handler */}
onBlur={/* Internal blur handler */}
/>Manual expansion (don't do this):
// ❌ Don't do this (use {...register('email')} instead)
const emailProps = register('email');
<input
name={emailProps.name}
ref={emailProps.ref}
onChange={emailProps.onChange}
onBlur={emailProps.onBlur}
/>Spread syntax is cleaner:
// ✅ Do this
<input {...register('email')} />Register with options:
<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email',
},
})}
/>But with Zod, options aren't needed (Zod handles validation).
Two-step validation:
Step 1: Schema definition
const signInSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Must be 8+ characters'),
});Step 2: Apply to form
useForm({
resolver: zodResolver(signInSchema),
});Validation flow:
User fills form
↓
User submits
↓
handleSubmit runs
↓
Zod validates data
↓
Valid? → onSubmit runs
Invalid? → Errors showExample validation:
// User enters:
email: 'bad-email'
password: '123'
// Zod validates:
email: 'bad-email' → ❌ Not a valid email
password: '123' → ❌ Less than 8 characters
// Errors object:
{
email: { message: 'Please enter a valid email address' },
password: { message: 'Password must be at least 8 characters' }
}
// UI shows both errors
// onSubmit does NOT runErrors are automatic:
const { formState: { errors } } = useForm();
// Check if field has error
if (errors.email) {
// Show error message
<p>{errors.email.message}</p>
}Conditional styling:
<input
className={`form-input ${errors.email ? 'form-input-error' : ''}`}
/>Result:
No error: class="form-input"
Has error: class="form-input form-input-error"Error display pattern:
{errors.email && (
<p className="form-error">{errors.email.message}</p>
)}Timeline:
Initial state: errors = {}
→ No error message shown
User submits with invalid email
→ errors = { email: { message: 'Invalid email' } }
→ Error message shown
User fixes email
→ errors = {}
→ Error message hiddenMultiple errors:
// Field has multiple validation rules
z.string()
.min(1, 'Required')
.email('Invalid email')
.max(100, 'Too long')
// Zod shows FIRST error that fails:
'' → 'Required'
'short' → 'Invalid email'
'test@x.com' → (no error)
'test@...(very long)' → 'Too long'Zod Schema Deep Dive
Testing the Form
Test valid submission:
- Enter valid email:
test@example.com - Enter valid password:
password123 - Click "Sign In"
- Check console: Should log form data
✅ No errors, data logged!
Test email validation:
- Enter invalid email:
bad-email - Enter any password
- Click "Sign In"
- Should see error: "Please enter a valid email address"
✅ Error shown, form NOT submitted!
Test password validation:
- Enter valid email
- Enter short password:
123 - Click "Sign In"
- Should see error: "Password must be at least 8 characters"
✅ Error shown, form NOT submitted!
Test empty fields:
- Leave both fields empty
- Click "Sign In"
- Should see errors on both fields:
- Email: "Email is required"
- Password: "Password is required"
✅ Multiple errors shown!
Test disabled state:
- Fill form with valid data
- Click "Sign In"
- Button should show "Signing in..." and be disabled
- After console.log, button returns to "Sign In"
✅ Button disabled during submission!
What's Next?
In Lesson 8, we'll:
- Add the SignInForm to SignInPage
- Remove the placeholder
- Test the complete sign-in page
- Add finishing touches (remember me, forgot password)
✅ Lesson Complete! You've created a powerful, validated form!
Key Takeaways
- ✅ React Hook Form reduces boilerplate code significantly
- ✅ Zod schemas provide type-safe validation
- ✅
registerconnects inputs to form state - ✅
handleSubmitvalidates before calling onSubmit - ✅
errorsobject shows validation errors automatically - ✅
isSubmittingtracks submission state - ✅ Spread syntax
{...register('name')}registers fields - ✅ Conditional CSS shows error states visually
- ✅ Error messages come from Zod schema
- ✅ Type safety with TypeScript integration