Form State Management
Form State Management
Efficiently manage form state, validation, and submission.
React Hook Form
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be at least 18'),
items: z.array(z.object({
name: z.string().min(1),
quantity: z.number().min(1),
})),
});
function OrderForm() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting, isDirty },
watch,
setValue,
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: '',
email: '',
age: 0,
items: [{ name: '', quantity: 1 }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
const onSubmit = async (data) => {
try {
await submitOrder(data);
showToast('Order submitted!');
} catch (error) {
showToast('Failed to submit order', 'error');
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input id="name" {...register('name')} />
{errors.name && <span className="error">{errors.name.message}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register('email')} />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<label htmlFor="age">Age</label>
<input id="age" type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span className="error">{errors.age.message}</span>}
</div>
<div>
<h3>Items</h3>
{fields.map((field, index) => (
<div key={field.id} className="item-row">
<input
{...register(`items.${index}.name`)}
placeholder="Item name"
/>
<input
type="number"
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
/>
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', quantity: 1 })}>
Add Item
</button>
</div>
<button type="submit" disabled={isSubmitting || !isDirty}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Formik
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';
const validationSchema = Yup.object({
firstName: Yup.string().required('Required'),
lastName: Yup.string().required('Required'),
email: Yup.string().email('Invalid email').required('Required'),
});
function SignupForm() {
return (
<Formik
initialValues={{ firstName: '', lastName: '', email: '' }}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<label htmlFor="firstName">First Name</label>
<Field id="firstName" name="firstName" />
<ErrorMessage name="firstName" component="span" className="error" />
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<Field id="lastName" name="lastName" />
<ErrorMessage name="lastName" component="span" className="error" />
</div>
<div>
<label htmlFor="email">Email</label>
<Field id="email" name="email" type="email" />
<ErrorMessage name="email" component="span" className="error" />
</div>
<button type="submit" disabled={isSubmitting}>
Submit
</button>
</Form>
)}
</Formik>
);
}
Choosing a Library
| Library | Best For |
|---|---|
| React Hook Form | Performance, minimal re-renders |
| Formik | Simple forms, easy to learn |
| Final Form | Subscription-based updates |
| Custom hooks | Full control, specific needs |
Validation Libraries
Validation Libraries
Zod
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
email: z.string().email('Invalid email'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[0-9]/, 'Must contain number'),
age: z.number().min(18).max(120),
role: z.enum(['user', 'admin']),
website: z.string().url().optional(),
tags: z.array(z.string()).min(1, 'At least one tag required'),
});
// Inferred type
type User = z.infer<typeof userSchema>;
// Validate data
const result = userSchema.safeParse(formData);
if (!result.success) {
console.error(result.error.format());
}
Yup
import * as Yup from 'yup';
const validationSchema = Yup.object({
username: Yup.string()
.required('Username is required')
.min(3, 'Must be at least 3 characters')
.max(20, 'Must be 20 characters or less'),
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.required('Password is required')
.min(8, 'Password must be at least 8 characters')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/,
'Must contain uppercase, lowercase, and number'
),
confirmPassword: Yup.string()
.oneOf([Yup.ref('password')], 'Passwords must match')
.required('Please confirm your password'),
birthDate: Yup.date()
.max(new Date(), 'Birth date cannot be in the future')
.required('Birth date is required'),
});
Custom Validation
// Async validation
const checkEmailUnique = async (email) => {
const response = await fetch(`/api/check-email?email=${email}`);
const { exists } = await response.json();
return !exists;
};
const schemaWithAsync = z.object({
email: z.string().email().refine(
async (email) => await checkEmailUnique(email),
'Email already in use'
),
});
// Cross-field validation
const passwordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{ message: 'Passwords do not match', path: ['confirmPassword'] }
);
Form Wizards
Form Wizards
Multi-step forms with progress tracking and state persistence.
Wizard Hook
// hooks/useFormWizard.js
function useFormWizard(steps) {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({});
const updateData = useCallback((stepData) => {
setFormData(prev => ({ ...prev, ...stepData }));
}, []);
const validateCurrentStep = useCallback(async () => {
const step = steps[currentStep];
if (!step.validate) return true;
try {
await step.validate(formData);
setErrors(prev => ({ ...prev, [currentStep]: null }));
return true;
} catch (error) {
setErrors(prev => ({ ...prev, [currentStep]: error }));
return false;
}
}, [currentStep, steps, formData]);
const nextStep = useCallback(async () => {
const isValid = await validateCurrentStep();
if (isValid && currentStep < steps.length - 1) {
setCurrentStep(prev => prev + 1);
}
}, [currentStep, steps.length, validateCurrentStep]);
const prevStep = useCallback(() => {
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
}, [currentStep]);
const goToStep = useCallback(async (step) => {
// Validate all steps before the target
for (let i = 0; i < step; i++) {
const isValid = await validateCurrentStep();
if (!isValid) return;
}
setCurrentStep(step);
}, [validateCurrentStep]);
return {
currentStep,
totalSteps: steps.length,
formData,
errors,
updateData,
nextStep,
prevStep,
goToStep,
isFirstStep: currentStep === 0,
isLastStep: currentStep === steps.length - 1,
progress: ((currentStep + 1) / steps.length) * 100,
};
}
Wizard Component
// components/FormWizard.jsx
function FormWizard({ steps, onSubmit }) {
const wizard = useFormWizard(steps);
const CurrentStepComponent = steps[wizard.currentStep].component;
return (
<div className="form-wizard">
<div className="wizard-progress">
<div className="progress-bar" style={{ width: `${wizard.progress}%` }} />
<div className="step-indicators">
{steps.map((step, index) => (
<button
key={index}
className={`step-indicator ${
index === wizard.currentStep ? 'active' : ''
} ${index < wizard.currentStep ? 'completed' : ''}`}
onClick={() => wizard.goToStep(index)}
disabled={index > wizard.currentStep}
>
{index + 1}
</button>
))}
</div>
</div>
<div className="wizard-content">
<CurrentStepComponent
data={wizard.formData}
errors={wizard.errors[wizard.currentStep]}
onChange={wizard.updateData}
/>
</div>
<div className="wizard-actions">
{!wizard.isFirstStep && (
<button onClick={wizard.prevStep}>Previous</button>
)}
{wizard.isLastStep ? (
<button onClick={() => onSubmit(wizard.formData)}>
Submit
</button>
) : (
<button onClick={wizard.nextStep}>Next</button>
)}
</div>
</div>
);
}
// Usage
const steps = [
{ component: PersonalInfoStep, validate: validatePersonalInfo },
{ component: AddressStep, validate: validateAddress },
{ component: PaymentStep, validate: validatePayment },
{ component: ReviewStep },
];
<FormWizard steps={steps} onSubmit={handleSubmit} />
State Persistence
// Persist wizard state in localStorage
function usePersistedWizard(steps) {
const wizard = useFormWizard(steps);
useEffect(() => {
localStorage.setItem('wizardState', JSON.stringify({
currentStep: wizard.currentStep,
formData: wizard.formData,
}));
}, [wizard.currentStep, wizard.formData]);
useEffect(() => {
const saved = localStorage.getItem('wizardState');
if (saved) {
const state = JSON.parse(saved);
// Restore state...
}
}, []);
return wizard;
}
Practice Problems
Create a reusable React component implementing Form Architecture. Include proper state management and accessibility.
Solution
// Production-ready component with:
// - Proper TypeScript types
// - Accessibility (ARIA)
// - Error boundaries
// - Loading states
// - Memoization where neededWrite unit and integration tests for Form Architecture using React Testing Library.
Solution
// Test coverage:
// 1. Rendering tests
// 2. Interaction tests
// 3. Edge case tests
// 4. Accessibility testsOptimize Form Architecture for performance. Consider memoization, code splitting, and bundle size.
Solution
// Optimization techniques:
// 1. React.memo / useMemo / useCallback
// 2. Code splitting with lazy()
// 3. Virtual scrolling for lists
// 4. Image lazy loading
// 5. Bundle analysisQuiz
1. What is the main advantage of React Hook Form?
2. Why use Zod for validation?
3. What is cross-field validation?
4. How should form wizards handle navigation?
5. Why persist wizard state?
Flashcards
Question
What is the difference between React Hook Form and Formik?
Click to reveal answer
Answer
React Hook Form uses uncontrolled components for performance; Formik uses controlled components for simplicity.
Question
What is Zod?
Click to reveal answer
Answer
A TypeScript-first schema validation library that provides type inference from schemas.
Question
What is useFieldArray?
Click to reveal answer
Answer
A React Hook Form hook for managing dynamic array fields like add/remove items.
Question
How do form wizards handle state?
Click to reveal answer
Answer
They track current step, accumulate form data, and validate each step before proceeding.
Question
What is Form Architecture?
Click to reveal answer
Answer
Form Architecture is a key concept in frontend development.
Revision Notes
Key Takeaways
- 1.React Hook Form offers better performance with uncontrolled components
- 2.Zod provides TypeScript inference and composable validation schemas
- 3.Always validate before allowing navigation in form wizards
- 4.Persist wizard state to prevent data loss
- 5.Use cross-field validation for related inputs
Interview Tips
- •Explain the difference between controlled and uncontrolled components
- •Discuss form validation strategies (client vs server)
- •Know how to implement multi-step forms with state management
Cheat Sheet
Form Architecture Cheat Sheet
React Hook Form
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema)
});
Zod Schema
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
Form Wizard
- Track current step
- Validate before proceeding
- Accumulate form data
- Persist state in localStorage
Key Concepts
- Controlled vs uncontrolled components
- Client vs server validation
- Async validation
- Cross-field validation