20 minlesson

Form Patterns and Best Practices

Form Patterns and Best Practices

Now that you understand form basics and React 19's new features, let's explore advanced patterns for building robust, accessible forms.

Single Handler for Multiple Inputs

Use name attribute to handle all inputs with one function:

jsx
1function RegistrationForm() {
2 const [formData, setFormData] = useState({
3 firstName: '',
4 lastName: '',
5 email: '',
6 password: '',
7 agreeToTerms: false
8 });
9
10 const handleChange = (e) => {
11 const { name, value, type, checked } = e.target;
12 setFormData(prev => ({
13 ...prev,
14 [name]: type === 'checkbox' ? checked : value
15 }));
16 };
17
18 return (
19 <form>
20 <input name="firstName" value={formData.firstName} onChange={handleChange} />
21 <input name="lastName" value={formData.lastName} onChange={handleChange} />
22 <input name="email" type="email" value={formData.email} onChange={handleChange} />
23 <input name="password" type="password" value={formData.password} onChange={handleChange} />
24 <input
25 name="agreeToTerms"
26 type="checkbox"
27 checked={formData.agreeToTerms}
28 onChange={handleChange}
29 />
30 </form>
31 );
32}

Uncontrolled Components with Refs

Sometimes you don't need state for every keystroke:

jsx
1function QuickForm() {
2 const inputRef = useRef(null);
3
4 const handleSubmit = (e) => {
5 e.preventDefault();
6 console.log('Value:', inputRef.current.value);
7 };
8
9 return (
10 <form onSubmit={handleSubmit}>
11 <input ref={inputRef} defaultValue="Initial" />
12 <button type="submit">Submit</button>
13 </form>
14 );
15}

Use uncontrolled when:

  • Value only needed on submit
  • Integrating with non-React code
  • File inputs (always uncontrolled)

File Input Handling

File inputs are always uncontrolled:

jsx
1function FileUpload() {
2 const [file, setFile] = useState(null);
3 const [preview, setPreview] = useState(null);
4
5 const handleChange = (e) => {
6 const selectedFile = e.target.files[0];
7 setFile(selectedFile);
8
9 // Create preview for images
10 if (selectedFile?.type.startsWith('image/')) {
11 const reader = new FileReader();
12 reader.onload = (e) => setPreview(e.target.result);
13 reader.readAsDataURL(selectedFile);
14 }
15 };
16
17 return (
18 <div>
19 <input type="file" accept="image/*" onChange={handleChange} />
20 {preview && <img src={preview} alt="Preview" style={{ maxWidth: 200 }} />}
21 {file && <p>Selected: {file.name} ({file.size} bytes)</p>}
22 </div>
23 );
24}

Validation Strategies

1. Validate on Submit

jsx
1function FormWithSubmitValidation() {
2 const [email, setEmail] = useState('');
3 const [errors, setErrors] = useState({});
4
5 const validate = () => {
6 const newErrors = {};
7 if (!email) newErrors.email = 'Email is required';
8 else if (!/\S+@\S+\.\S+/.test(email)) newErrors.email = 'Invalid email';
9 return newErrors;
10 };
11
12 const handleSubmit = (e) => {
13 e.preventDefault();
14 const newErrors = validate();
15 if (Object.keys(newErrors).length > 0) {
16 setErrors(newErrors);
17 return;
18 }
19 // Submit...
20 };
21
22 return (
23 <form onSubmit={handleSubmit}>
24 <input value={email} onChange={e => setEmail(e.target.value)} />
25 {errors.email && <span className="error">{errors.email}</span>}
26 <button type="submit">Submit</button>
27 </form>
28 );
29}

2. Validate on Blur (Field Exit)

jsx
1function FormWithBlurValidation() {
2 const [email, setEmail] = useState('');
3 const [touched, setTouched] = useState(false);
4
5 const error = touched && !email.includes('@') ? 'Invalid email' : null;
6
7 return (
8 <form>
9 <input
10 value={email}
11 onChange={e => setEmail(e.target.value)}
12 onBlur={() => setTouched(true)}
13 />
14 {error && <span className="error">{error}</span>}
15 </form>
16 );
17}

3. Real-time Validation

jsx
1function FormWithRealtimeValidation() {
2 const [password, setPassword] = useState('');
3
4 const checks = {
5 length: password.length >= 8,
6 uppercase: /[A-Z]/.test(password),
7 lowercase: /[a-z]/.test(password),
8 number: /\d/.test(password),
9 };
10
11 const isValid = Object.values(checks).every(Boolean);
12
13 return (
14 <div>
15 <input
16 type="password"
17 value={password}
18 onChange={e => setPassword(e.target.value)}
19 />
20 <ul className="password-checks">
21 <li className={checks.length ? 'valid' : ''}>At least 8 characters</li>
22 <li className={checks.uppercase ? 'valid' : ''}>One uppercase letter</li>
23 <li className={checks.lowercase ? 'valid' : ''}>One lowercase letter</li>
24 <li className={checks.number ? 'valid' : ''}>One number</li>
25 </ul>
26 <button disabled={!isValid}>Create Account</button>
27 </div>
28 );
29}

Error Display Patterns

Field-Level Errors

jsx
1function FormField({ label, name, error, ...props }) {
2 const id = `field-${name}`;
3 const errorId = `${id}-error`;
4
5 return (
6 <div className="form-field">
7 <label htmlFor={id}>{label}</label>
8 <input
9 id={id}
10 name={name}
11 aria-invalid={!!error}
12 aria-describedby={error ? errorId : undefined}
13 {...props}
14 />
15 {error && (
16 <span id={errorId} className="error" role="alert">
17 {error}
18 </span>
19 )}
20 </div>
21 );
22}

Form-Level Error Summary

jsx
1function FormWithSummary({ errors }) {
2 const hasErrors = Object.keys(errors).length > 0;
3
4 return (
5 <>
6 {hasErrors && (
7 <div className="error-summary" role="alert">
8 <h3>Please fix the following errors:</h3>
9 <ul>
10 {Object.entries(errors).map(([field, message]) => (
11 <li key={field}>
12 <a href={`#field-${field}`}>{message}</a>
13 </li>
14 ))}
15 </ul>
16 </div>
17 )}
18 {/* Form fields... */}
19 </>
20 );
21}

Accessible Forms

Build forms that work for everyone:

jsx
1function AccessibleForm() {
2 return (
3 <form aria-label="Contact form">
4 {/* Labels with htmlFor */}
5 <div>
6 <label htmlFor="name">Name (required)</label>
7 <input
8 id="name"
9 name="name"
10 required
11 aria-required="true"
12 />
13 </div>
14
15 {/* Field with description */}
16 <div>
17 <label htmlFor="email">Email</label>
18 <input
19 id="email"
20 name="email"
21 type="email"
22 aria-describedby="email-hint"
23 />
24 <small id="email-hint">We'll never share your email</small>
25 </div>
26
27 {/* Radio group with fieldset */}
28 <fieldset>
29 <legend>Contact preference</legend>
30 <label>
31 <input type="radio" name="contact" value="email" />
32 Email
33 </label>
34 <label>
35 <input type="radio" name="contact" value="phone" />
36 Phone
37 </label>
38 </fieldset>
39
40 {/* Error announcement */}
41 <div role="alert" aria-live="polite">
42 {/* Errors will be announced by screen readers */}
43 </div>
44
45 <button type="submit">Submit</button>
46 </form>
47 );
48}

Key accessibility practices:

  • Always use <label> with htmlFor
  • Use aria-invalid for error states
  • Use aria-describedby for hints and errors
  • Use fieldset/legend for radio groups
  • Use role="alert" for error messages

Form State Management Patterns

useReducer for Complex Forms

jsx
1const initialState = {
2 values: { email: '', password: '' },
3 errors: {},
4 touched: {},
5 isSubmitting: false
6};
7
8function formReducer(state, action) {
9 switch (action.type) {
10 case 'SET_FIELD':
11 return {
12 ...state,
13 values: { ...state.values, [action.field]: action.value }
14 };
15 case 'SET_ERROR':
16 return {
17 ...state,
18 errors: { ...state.errors, [action.field]: action.error }
19 };
20 case 'TOUCH_FIELD':
21 return {
22 ...state,
23 touched: { ...state.touched, [action.field]: true }
24 };
25 case 'SUBMIT_START':
26 return { ...state, isSubmitting: true };
27 case 'SUBMIT_END':
28 return { ...state, isSubmitting: false };
29 case 'RESET':
30 return initialState;
31 default:
32 return state;
33 }
34}
35
36function ComplexForm() {
37 const [state, dispatch] = useReducer(formReducer, initialState);
38
39 const handleChange = (e) => {
40 dispatch({
41 type: 'SET_FIELD',
42 field: e.target.name,
43 value: e.target.value
44 });
45 };
46
47 // ... rest of form
48}

Custom useForm Hook

jsx
1function useForm(initialValues, validate) {
2 const [values, setValues] = useState(initialValues);
3 const [errors, setErrors] = useState({});
4 const [touched, setTouched] = useState({});
5
6 const handleChange = (e) => {
7 const { name, value, type, checked } = e.target;
8 setValues(prev => ({
9 ...prev,
10 [name]: type === 'checkbox' ? checked : value
11 }));
12 };
13
14 const handleBlur = (e) => {
15 setTouched(prev => ({ ...prev, [e.target.name]: true }));
16 if (validate) {
17 setErrors(validate(values));
18 }
19 };
20
21 const handleSubmit = (onSubmit) => (e) => {
22 e.preventDefault();
23 const validationErrors = validate ? validate(values) : {};
24 setErrors(validationErrors);
25 setTouched(Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
26
27 if (Object.keys(validationErrors).length === 0) {
28 onSubmit(values);
29 }
30 };
31
32 const reset = () => {
33 setValues(initialValues);
34 setErrors({});
35 setTouched({});
36 };
37
38 return {
39 values,
40 errors,
41 touched,
42 handleChange,
43 handleBlur,
44 handleSubmit,
45 reset
46 };
47}

Third-Party Form Libraries

For complex forms, consider:

React Hook Form - Performant, minimal re-renders:

jsx
1import { useForm } from 'react-hook-form';
2
3function Form() {
4 const { register, handleSubmit, errors } = useForm();
5
6 return (
7 <form onSubmit={handleSubmit(data => console.log(data))}>
8 <input {...register('email', { required: true })} />
9 {errors.email && <span>Email required</span>}
10 </form>
11 );
12}

Formik - Full-featured form management:

jsx
1import { Formik, Form, Field, ErrorMessage } from 'formik';
2
3function MyForm() {
4 return (
5 <Formik
6 initialValues={{ email: '' }}
7 validate={values => {
8 const errors = {};
9 if (!values.email) errors.email = 'Required';
10 return errors;
11 }}
12 onSubmit={values => console.log(values)}
13 >
14 <Form>
15 <Field name="email" type="email" />
16 <ErrorMessage name="email" />
17 <button type="submit">Submit</button>
18 </Form>
19 </Formik>
20 );
21}

Summary

Form best practices:

  1. Use controlled inputs for most cases
  2. Single handler with name attribute
  3. Validate appropriately - submit, blur, or real-time
  4. Display errors clearly at field and form level
  5. Build accessible forms with proper ARIA
  6. Consider React 19 actions for server integration
  7. Use libraries for complex forms

Next, let's build a multi-step registration form that applies all these patterns!