React & Next.js Topic
React Forms & Controlled Components
1. Controlled Components
Each form input is tied to React state via value and onChange. The component fully controls the input.
Basic Controlled Inputs
jsx
import React, { useState } from 'react';
function BasicForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
bio: '',
country: '',
subscribe: false,
gender: '',
age: ''
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
{/* Text Input */}
<div>
<label>First Name:</label>
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleChange}
required
/>
</div>
{/* Email Input */}
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
{/* Password Input */}
<div>
<label>Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
minLength="6"
required
/>
</div>
{/* Textarea */}
<div>
<label>Bio:</label>
<textarea
name="bio"
value={formData.bio}
onChange={handleChange}
rows="4"
/>
</div>
{/* Select Dropdown */}
<div>
<label>Country:</label>
<select
name="country"
value={formData.country}
onChange={handleChange}
required
>
<option value="">Select Country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
</select>
</div>
{/* Checkbox */}
<div>
<label>
<input
type="checkbox"
name="subscribe"
checked={formData.subscribe}
onChange={handleChange}
/>
Subscribe to newsletter
</label>
</div>
{/* Radio Buttons */}
<div>
<label>Gender:</label>
<label>
<input
type="radio"
name="gender"
value="male"
checked={formData.gender === 'male'}
onChange={handleChange}
/>
Male
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={formData.gender === 'female'}
onChange={handleChange}
/>
Female
</label>
</div>
{/* Number Input */}
<div>
<label>Age:</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
min="0"
max="120"
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
2. Advanced Input Handling
Custom Hook for Form Management
jsx
import { useState, useCallback } from 'react';
function useForm(initialState, validations) {
const [formData, setFormData] = useState(initialState);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
}, [errors]);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
// Validate field on blur
if (validations && validations[name]) {
const error = validations[name](formData[name]);
setErrors(prev => ({ ...prev, [name]: error }));
}
}, [formData, validations]);
const setFieldValue = useCallback((name, value) => {
setFormData(prev => ({ ...prev, [name]: value }));
}, []);
const resetForm = useCallback(() => {
setFormData(initialState);
setErrors({});
setTouched({});
}, [initialState]);
return {
formData,
errors,
touched,
handleChange,
handleBlur,
setFieldValue,
resetForm,
setFormData
};
}
3. Validation Patterns
Client-Side Validation
jsx
import React, { useState } from 'react';
// Validation functions
const validations = {
email: (value) => {
if (!value) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
return '';
},
password: (value) => {
if (!value) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value))
return 'Password must contain uppercase, lowercase and number';
return '';
},
confirmPassword: (value, allValues) => {
if (!value) return 'Please confirm password';
if (value !== allValues.password) return 'Passwords do not match';
return '';
}
};
function ValidationForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const validateField = (name, value) => {
if (validations[name]) {
return validations[name](value, formData);
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Validate on change if field was already touched
if (touched[name]) {
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
}
};
const handleBlur = (e) => {
const { name, value } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};
const validateForm = () => {
const newErrors = {};
let isValid = true;
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
isValid = false;
}
});
setErrors(newErrors);
setTouched(Object.keys(formData).reduce((acc, key) => ({
...acc, [key]: true
}), {}));
return isValid;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Form is valid!', formData);
} else {
console.log('Form has errors');
}
};
const getFieldClass = (name) => {
return `form-field ${errors[name] ? 'error' : ''} ${touched[name] ? 'touched' : ''}`;
};
return (
<form onSubmit={handleSubmit} className="form">
<div className={getFieldClass('email')}>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={errors.email ? 'input-error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className={getFieldClass('password')}>
<label>Password:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={errors.password ? 'input-error' : ''}
/>
{errors.password && <span className="error-message">{errors.password}</span>}
</div>
<div className={getFieldClass('confirmPassword')}>
<label>Confirm Password:</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
onBlur={handleBlur}
className={errors.confirmPassword ? 'input-error' : ''}
/>
{errors.confirmPassword && (
<span className="error-message">{errors.confirmPassword}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
4. React Hook Form
Uses uncontrolled components internally with refs, minimizing re-renders. State is managed by the library, not React.
Basic Usage
jsx
import React from 'react';
import { useForm } from 'react-hook-form';
function ReactHookFormExample() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isValid, touchedFields },
watch,
reset,
setValue,
getValues
} = useForm({
mode: 'onChange', // Validate on change
defaultValues: {
firstName: '',
email: '',
age: 18
}
});
const onSubmit = async (data) => {
console.log('Form data:', data);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
reset();
};
// Watch specific field
const watchPassword = watch('password');
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Text Input with Validation */}
<div>
<label>First Name:</label>
<input
{...register('firstName', {
required: 'First name is required',
minLength: {
value: 2,
message: 'Minimum 2 characters'
}
})}
/>
{errors.firstName && (
<span className="error">{errors.firstName.message}</span>
)}
</div>
{/* Email with Pattern Validation */}
<div>
<label>Email:</label>
<input
type="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Invalid email address'
}
})}
/>
{errors.email && (
<span className="error">{errors.email.message}</span>
)}
</div>
{/* Conditional Fields */}
<div>
<label>Create Password:</label>
<input
type="password"
{...register('password', {
required: 'Password is required',
minLength: {
value: 6,
message: 'Minimum 6 characters'
}
})}
/>
{errors.password && (
<span className="error">{errors.password.message}</span>
)}
</div>
{/* Confirm Password with Custom Validation */}
<div>
<label>Confirm Password:</label>
<input
type="password"
{...register('confirmPassword', {
required: 'Please confirm password',
validate: value =>
value === watchPassword || 'Passwords do not match'
})}
/>
{errors.confirmPassword && (
<span className="error">{errors.confirmPassword.message}</span>
)}
</div>
{/* Number Input */}
<div>
<label>Age:</label>
<input
type="number"
{...register('age', {
min: {
value: 18,
message: 'Must be at least 18 years old'
},
max: {
value: 100,
message: 'Must be less than 100'
}
})}
/>
{errors.age && <span className="error">{errors.age.message}</span>}
</div>
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Advanced React Hook Form
jsx
import { useForm, useFieldArray, Controller } from 'react-hook-form';
import Select from 'react-select';
function AdvancedReactHookForm() {
const {
control,
register,
handleSubmit,
formState: { errors },
watch
} = useForm({
defaultValues: {
users: [{ firstName: '', lastName: '' }],
country: '',
preferences: []
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'users'
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Dynamic Fields */}
<div>
<h3>Users</h3>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`users.${index}.firstName`)}
placeholder="First Name"
/>
<input
{...register(`users.${index}.lastName`)}
placeholder="Last Name"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ firstName: '', lastName: '' })}>
Add User
</button>
</div>
{/* Controlled Component with Controller */}
<div>
<label>Country:</label>
<Controller
name="country"
control={control}
render={({ field }) => (
<Select
{...field}
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' }
]}
/>
)}
/>
</div>
{/* File Input */}
<div>
<label>Profile Picture:</label>
<input
type="file"
{...register('avatar', {
validate: {
lessThan1MB: (files) =>
files[0]?.size < 1000000 || 'Max 1MB',
acceptedFormats: (files) =>
['image/jpeg', 'image/png'].includes(files[0]?.type) ||
'Only JPEG/PNG'
}
})}
/>
{errors.avatar && <span className="error">{errors.avatar.message}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
}
5. Formik
Provides a higher-level abstraction for form state and validation. Feels similar to Redux patterns (centralized state).
Basic Formik Usage
jsx
import { useFormik } from 'formik';
import * as Yup from 'yup';
function FormikExample() {
const formik = useFormik({
initialValues: {
email: '',
password: '',
rememberMe: false
},
validationSchema: Yup.object({
email: Yup.string()
.email('Invalid email address')
.required('Required'),
password: Yup.string()
.min(6, 'Must be at least 6 characters')
.required('Required')
}),
onSubmit: (values) => {
console.log('Form data:', values);
}
});
return (
<form onSubmit={formik.handleSubmit}>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? (
<div className="error">{formik.errors.email}</div>
) : null}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password ? (
<div className="error">{formik.errors.password}</div>
) : null}
</div>
<div>
<label>
<input
type="checkbox"
name="rememberMe"
checked={formik.values.rememberMe}
onChange={formik.handleChange}
/>
Remember me
</label>
</div>
<button type="submit">Submit</button>
</form>
);
}
6. Performance Optimization
Memoized Form Components
jsx
import React, { memo, useCallback } from 'react';
const FormInput = memo(({
label,
name,
value,
error,
touched,
onChange,
onBlur,
...props
}) => {
console.log(`Rendering ${name}`);
return (
<div className={`form-field ${error && touched ? 'error' : ''}`}>
<label htmlFor={name}>{label}:</label>
<input
id={name}
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
{...props}
/>
{error && touched && <span className="error-message">{error}</span>}
</div>
);
});
function OptimizedForm() {
const [formData, setFormData] = useState({ /* ... */ });
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
// Memoized handlers
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
}, []);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
// Validate field...
}, []);
return (
<form>
<FormInput
label="First Name"
name="firstName"
value={formData.firstName}
error={errors.firstName}
touched={touched.firstName}
onChange={handleChange}
onBlur={handleBlur}
/>
{/* More optimized inputs... */}
</form>
);
}
Comparison Summary
| Feature | Controlled Components | React Hook Form | Formik |
|---|---|---|---|
| Performance | Good with optimization | Excellent | Good |
| Bundle Size | 0kB | ~9kB | ~13kB |
| Learning Curve | Low | Medium | Medium |
| Validation | Manual/Yup | Built-in/Yup | Yup-based |
| TypeScript | Excellent | Excellent | Good |
| Dynamic Fields | Manual | useFieldArray | Manual |
Choose based on your needs:
-
Controlled Components: Simple forms, learning purposes
-
React Hook Form: Complex forms, performance critical
-
Formik: Familiar API, Redux-like pattern preference
Comments (0)
No comments yet. Be the first to comment!