Skip to main content

Chapter 4: Event Handling & Forms - Building Interactive React Applications

Welcome to the comprehensive guide to event handling and form management in React! In this chapter, we'll explore everything you need to know about creating interactive user interfaces and managing user input effectively.

Learning Objectives

By the end of this chapter, you will understand:

  • What event handling means in React and why it's crucial for interactive applications
  • How to handle different types of events effectively and efficiently
  • Why React's synthetic events exist and how they improve performance
  • What form management involves and how to implement it properly
  • How to choose between controlled and uncontrolled components for different scenarios
  • Why validation matters and how to implement robust form validation
  • What best practices are for building user-friendly forms and interactions

What is Event Handling? The Foundation of Interactive React Applications

What is Event Handling in React?

Event handling in React is the process of responding to user interactions like clicks, form submissions, keyboard input, and other browser events. It's what makes your React components interactive and responsive to user actions.

Event handling is what transforms static React components into dynamic, interactive user interfaces. Without event handling, your components would be unable to respond to user input or trigger actions.

What Makes React Event Handling Different?

React's event handling differs from traditional DOM event handling in several key ways:

  1. Synthetic Events: React wraps native events in SyntheticEvent objects
  2. Event Delegation: React uses event delegation for better performance
  3. Cross-browser Compatibility: Synthetic events provide consistent behavior across browsers
  4. Automatic Cleanup: React handles event listener cleanup automatically
  5. CamelCase Naming: Event names use camelCase (onClick, onChange, etc.)

What Types of Events Can You Handle?

React supports a wide range of events:

  • Mouse Events: onClick, onMouseOver, onMouseOut, onDoubleClick
  • Keyboard Events: onKeyDown, onKeyUp, onKeyPress
  • Form Events: onChange, onSubmit, onFocus, onBlur
  • Touch Events: onTouchStart, onTouchEnd, onTouchMove
  • Drag Events: onDrag, onDragStart, onDragEnd
  • Scroll Events: onScroll
  • Window Events: onResize, onLoad, onUnload

How to Handle Events Effectively? The Technical Implementation

How to Handle Basic Events?

The most common way to handle events in React is using event handler functions:

function InteractiveButton() {
const handleClick = (event) => {
console.log('Button clicked!', event);
console.log('Event type:', event.type);
console.log('Target element:', event.target);
};

const handleMouseOver = (event) => {
console.log('Mouse over button');
event.target.style.backgroundColor = 'lightblue';
};

const handleMouseOut = (event) => {
console.log('Mouse left button');
event.target.style.backgroundColor = '';
};

return (
<button
onClick={handleClick}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
Interactive Button
</button>
);
}

How to Handle Events with Parameters?

When you need to pass additional data to event handlers, you can use arrow functions or bind methods:

function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build an app', completed: true },
{ id: 3, text: 'Deploy to production', completed: false }
]);

// Method 1: Arrow function in JSX
const handleToggle = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};

// Method 2: Using bind
const handleDelete = function(id) {
setTodos(todos.filter(todo => todo.id !== id));
};

// Method 3: Using useCallback for performance
const handleEdit = useCallback((id, newText) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}, [todos]);

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => handleDelete(todo.id)}>
Delete
</button>
<button onClick={() => handleEdit(todo.id, 'Updated text')}>
Edit
</button>
</li>
))}
</ul>
);
}

How to Handle Form Events?

Form events are crucial for user input handling:

function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});

const [errors, setErrors] = useState({});

const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));

// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ''
}));
}
};

const handleSubmit = (e) => {
e.preventDefault(); // Prevent default form submission

// Validate form
const newErrors = {};
if (!formData.name.trim()) newErrors.name = 'Name is required';
if (!formData.email.trim()) newErrors.email = 'Email is required';
if (!formData.message.trim()) newErrors.message = 'Message is required';

if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}

// Submit form
console.log('Form submitted:', formData);
// Reset form
setFormData({ name: '', email: '', message: '' });
setErrors({});
};

const handleFocus = (e) => {
console.log('Input focused:', e.target.name);
};

const handleBlur = (e) => {
console.log('Input blurred:', e.target.name);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error-message">{errors.name}</span>}
</div>

<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>

<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
className={errors.message ? 'error' : ''}
/>
{errors.message && <span className="error-message">{errors.message}</span>}
</div>

<button type="submit">Send Message</button>
</form>
);
}

How to Handle Keyboard Events?

Keyboard events are essential for accessibility and user experience:

function SearchBox({ onSearch }) {
const [query, setQuery] = useState('');

const handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
onSearch(query);
} else if (e.key === 'Escape') {
setQuery('');
}
};

const handleKeyUp = (e) => {
// Real-time search as user types
if (e.key !== 'Enter' && e.key !== 'Escape') {
onSearch(query);
}
};

return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
placeholder="Search..."
/>
);
}

function KeyboardShortcuts() {
const [shortcuts, setShortcuts] = useState([]);

useEffect(() => {
const handleKeyDown = (e) => {
// Check for Ctrl/Cmd + key combinations
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
console.log('Save shortcut triggered');
setShortcuts(prev => [...prev, 'Save']);
} else if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
console.log('Undo shortcut triggered');
setShortcuts(prev => [...prev, 'Undo']);
}
};

document.addEventListener('keydown', handleKeyDown);

return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, []);

return (
<div>
<h3>Keyboard Shortcuts</h3>
<p>Try Ctrl+S (Save) or Ctrl+Z (Undo)</p>
<ul>
{shortcuts.map((shortcut, index) => (
<li key={index}>{shortcut} triggered</li>
))}
</ul>
</div>
);
}

Why Use React's Synthetic Events? The Benefits Explained

Why Does React Use Synthetic Events?

React's synthetic events provide several advantages over native DOM events:

  1. Cross-browser Compatibility: Consistent behavior across all browsers
  2. Performance: Event delegation reduces memory usage
  3. Automatic Cleanup: No need to manually remove event listeners
  4. Normalized API: Same API regardless of browser differences
  5. Better Integration: Seamless integration with React's component system

Why is Event Delegation Important?

Event delegation improves performance by:

  • Reduced Memory Usage: Fewer event listeners in memory
  • Better Performance: Events are handled at the document level
  • Dynamic Content: Works with dynamically added elements
  • Simplified Management: No need to add/remove listeners for each element

Why Prevent Default Behavior?

Preventing default behavior is important for:

  • Form Submissions: Control when and how forms are submitted
  • Link Navigation: Prevent page navigation for single-page apps
  • Context Menus: Disable right-click menus when needed
  • Keyboard Shortcuts: Override browser default shortcuts

How to Manage Forms Effectively? Controlled vs Uncontrolled Components

What are Controlled Components?

Controlled components are form elements whose value is controlled by React state. The component's value is always in sync with the state.

Controlled components give you complete control over form data and validation.

How to Implement Controlled Components?

function ControlledForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false
});

const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);

const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};

const validateForm = () => {
const newErrors = {};

if (!formData.username.trim()) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}

if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}

if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}

if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}

if (!formData.agreeToTerms) {
newErrors.agreeToTerms = 'You must agree to the terms';
}

return newErrors;
};

const handleSubmit = async (e) => {
e.preventDefault();

const newErrors = validateForm();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}

setIsSubmitting(true);
try {
// Submit form data
await submitForm(formData);
console.log('Form submitted successfully');
// Reset form
setFormData({
username: '',
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false
});
setErrors({});
} catch (error) {
setErrors({ submit: error.message });
} finally {
setIsSubmitting(false);
}
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleInputChange}
className={errors.username ? 'error' : ''}
/>
{errors.username && <span className="error">{errors.username}</span>}
</div>

<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>

<div>
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>

<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>}
</div>

<div>
<label>
<input
name="agreeToTerms"
type="checkbox"
checked={formData.agreeToTerms}
onChange={handleInputChange}
/>
I agree to the terms and conditions
</label>
{errors.agreeToTerms && <span className="error">{errors.agreeToTerms}</span>}
</div>

{errors.submit && <div className="error">{errors.submit}</div>}

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}

What are Uncontrolled Components?

Uncontrolled components are form elements whose value is managed by the DOM itself, not by React state. You access their values using refs.

Uncontrolled components are useful when you don't need real-time validation or when integrating with non-React libraries.

How to Implement Uncontrolled Components?

function UncontrolledForm() {
const nameRef = useRef();
const emailRef = useRef();
const messageRef = useRef();

const handleSubmit = (e) => {
e.preventDefault();

const formData = {
name: nameRef.current.value,
email: emailRef.current.value,
message: messageRef.current.value
};

console.log('Form data:', formData);

// Reset form
nameRef.current.value = '';
emailRef.current.value = '';
messageRef.current.value = '';
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
ref={nameRef}
type="text"
defaultValue=""
/>
</div>

<div>
<label htmlFor="email">Email:</label>
<input
id="email"
ref={emailRef}
type="email"
defaultValue=""
/>
</div>

<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
ref={messageRef}
defaultValue=""
/>
</div>

<button type="submit">Submit</button>
</form>
);
}

Why Choose Between Controlled and Uncontrolled Components? The Benefits Explained

Why Use Controlled Components?

Controlled components are better when you need:

  1. Real-time Validation: Validate input as the user types
  2. Conditional Rendering: Show/hide elements based on form state
  3. Complex State Logic: Manage complex form state relationships
  4. Accessibility: Better screen reader support
  5. Testing: Easier to test form behavior

Why Use Uncontrolled Components?

Uncontrolled components are better when you need:

  1. Performance: Fewer re-renders for large forms
  2. Integration: Work with non-React libraries
  3. Simplicity: Simple forms without complex validation
  4. File Uploads: Handle file inputs more easily
  5. Legacy Code: Integrate with existing form libraries

When to Use Each Approach?

Use Controlled Components for:

  • Forms with real-time validation
  • Complex form state management
  • Forms that need to be reset programmatically
  • Forms with conditional fields

Use Uncontrolled Components for:

  • Simple forms without validation
  • File upload forms
  • Forms that need to integrate with non-React libraries
  • Performance-critical forms with many fields

How to Implement Form Validation? Best Practices for User Experience

What is Form Validation?

Form validation is the process of checking user input to ensure it meets certain criteria before submission. It's crucial for data integrity and user experience.

Form validation is what ensures your application receives clean, valid data and provides helpful feedback to users.

How to Implement Client-Side Validation?

function ValidatedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
phone: '',
website: ''
});

const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});

const validateField = (name, value) => {
switch (name) {
case 'name':
if (!value.trim()) return 'Name is required';
if (value.length < 2) return 'Name must be at least 2 characters';
return '';

case 'email':
if (!value.trim()) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Email is invalid';
return '';

case 'password':
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
return 'Password must contain uppercase, lowercase, and number';
}
return '';

case 'confirmPassword':
if (!value) return 'Please confirm your password';
if (value !== formData.password) return 'Passwords do not match';
return '';

case 'phone':
if (value && !/^\+?[\d\s\-\(\)]+$/.test(value)) {
return 'Phone number is invalid';
}
return '';

case 'website':
if (value && !/^https?:\/\/.+/.test(value)) {
return 'Website must start with http:// or https://';
}
return '';

default:
return '';
}
};

const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));

// Validate field if it has been 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 handleSubmit = (e) => {
e.preventDefault();

// Mark all fields as touched
const allTouched = Object.keys(formData).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);

// Validate all fields
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) newErrors[key] = error;
});

if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}

// Submit form
console.log('Form submitted:', formData);
};

return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name *</label>
<input
id="name"
name="name"
type="text"
value={formData.name}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.name ? 'error' : ''}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>

<div>
<label htmlFor="email">Email *</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.email ? 'error' : ''}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>

<div>
<label htmlFor="password">Password *</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.password ? 'error' : ''}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>

<div>
<label htmlFor="confirmPassword">Confirm Password *</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.confirmPassword ? 'error' : ''}
/>
{errors.confirmPassword && <span className="error">{errors.confirmPassword}</span>}
</div>

<div>
<label htmlFor="phone">Phone</label>
<input
id="phone"
name="phone"
type="tel"
value={formData.phone}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.phone ? 'error' : ''}
/>
{errors.phone && <span className="error">{errors.phone}</span>}
</div>

<div>
<label htmlFor="website">Website</label>
<input
id="website"
name="website"
type="url"
value={formData.website}
onChange={handleInputChange}
onBlur={handleBlur}
className={errors.website ? 'error' : ''}
/>
{errors.website && <span className="error">{errors.website}</span>}
</div>

<button type="submit">Submit</button>
</form>
);
}

Event Handling and Forms Best Practices

What Are Event Handling Best Practices?

  1. Use Event Delegation: Let React handle event delegation for better performance
  2. Prevent Default When Needed: Use preventDefault() for form submissions and links
  3. Use useCallback for Event Handlers: Optimize performance for frequently re-rendered components
  4. Handle Errors Gracefully: Always provide error handling for async operations
  5. Accessibility: Ensure keyboard navigation and screen reader support

How to Optimize Event Handling Performance?

function OptimizedComponent() {
const [items, setItems] = useState([]);

// Memoize event handlers to prevent unnecessary re-renders
const handleItemClick = useCallback((id) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, selected: !item.selected } : item
));
}, []);

const handleItemDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);

return (
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
onClick={handleItemClick}
onDelete={handleItemDelete}
/>
))}
</div>
);
}

What Are Form Best Practices?

  1. Progressive Enhancement: Start with basic HTML forms and enhance with JavaScript
  2. Real-time Validation: Provide immediate feedback as users type
  3. Clear Error Messages: Use descriptive, helpful error messages
  4. Accessibility: Ensure proper labeling and keyboard navigation
  5. Performance: Use debouncing for real-time validation
  6. Security: Always validate on the server side as well

Summary: Building Interactive React Applications

What Have We Learned?

In this chapter, we've explored the complete spectrum of event handling and form management in React:

  1. Event Handling: Synthetic events, event delegation, and performance optimization
  2. Form Management: Controlled vs uncontrolled components and their use cases
  3. Validation: Client-side validation patterns and custom validation hooks
  4. Best Practices: Performance optimization, accessibility, and user experience

How to Choose the Right Approach?

  1. Event Handling: Use React's synthetic events for consistency and performance
  2. Forms: Choose controlled components for complex validation, uncontrolled for simple forms
  3. Validation: Implement real-time validation with clear error messages
  4. Performance: Use useCallback and useMemo to optimize event handlers and form logic

Why This Matters for Your React Journey?

Understanding event handling and forms is crucial because:

  • User Interaction: Forms and events are how users interact with your application
  • Data Integrity: Proper validation ensures clean, reliable data
  • User Experience: Good form design and validation improve user satisfaction
  • Accessibility: Proper event handling and form design make your app accessible
  • Performance: Optimized event handling improves application performance

Next Steps

Now that you understand event handling and forms, you're ready to explore:

  • Advanced Hooks: How to use React hooks for complex scenarios
  • Performance Optimization: How to build fast, responsive applications
  • Testing: How to test components with complex interactions
  • Accessibility: How to build inclusive user interfaces

Remember: The best event handling and form management approach is the one that provides the best user experience while maintaining good performance and accessibility.

const handleSubmit = (event) => {
event.preventDefault();
console.log('Name:', nameRef.current.value);
console.log('Email:', emailRef.current.value);
};

return (
<form onSubmit={handleSubmit}>
<input
ref={nameRef}
type="text"
placeholder="Name"
/>
<input
ref={emailRef}
type="email"
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
);
}

Form Validation

Basic Validation

function ValidatedForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
});
const [errors, setErrors] = useState({});

const validateForm = () => {
const newErrors = {};

if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}

if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}

if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prev => ({ ...prev, [name]: value }));

// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};

const handleSubmit = (event) => {
event.preventDefault();
if (validateForm()) {
console.log('Form is valid:', formData);
}
};

return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>

<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>

<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>

<button type="submit">Submit</button>
</form>
);
}

Real-time Validation

function RealTimeValidation() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});

const validateField = (name, value) => {
switch (name) {
case 'email':
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return '';
case 'password':
if (!value) return 'Password is required';
if (value.length < 6) return 'Password must be at least 6 characters';
return '';
default:
return '';
}
};

const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prev => ({ ...prev, [name]: value }));

// Validate field in real-time
const error = validateField(name, value);
setErrors(prev => ({ ...prev, [name]: error }));
};

return (
<form>
<div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>

<div>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
</form>
);
}

Custom Form Hook

function useForm(initialValues, validationRules) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});

const setValue = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));

if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};

const setFieldTouched = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
};

const validate = () => {
const newErrors = {};

Object.keys(validationRules).forEach(field => {
const rule = validationRules[field];
const value = values[field];

if (rule.required && (!value || value.trim() === '')) {
newErrors[field] = rule.required;
} else if (rule.pattern && !rule.pattern.test(value)) {
newErrors[field] = rule.pattern.message;
}
});

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
if (validate()) {
onSubmit(values);
}
};

return {
values,
errors,
touched,
setValue,
setFieldTouched,
validate,
handleSubmit
};
}

// Usage
function ContactForm() {
const validationRules = {
name: { required: 'Name is required' },
email: {
required: 'Email is required',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'Please enter a valid email'
}
}
};

const {
values,
errors,
setValue,
setFieldTouched,
handleSubmit
} = useForm({ name: '', email: '' }, validationRules);

const onSubmit = (formValues) => {
console.log('Form submitted:', formValues);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="text"
name="name"
value={values.name}
onChange={(e) => setValue('name', e.target.value)}
onBlur={() => setFieldTouched('name')}
placeholder="Name"
/>
{errors.name && <span className="error">{errors.name}</span>}

<input
type="email"
name="email"
value={values.email}
onChange={(e) => setValue('email', e.target.value)}
onBlur={() => setFieldTouched('email')}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}

<button type="submit">Submit</button>
</form>
);
}

Form Libraries

React Hook Form

import { useForm } from 'react-hook-form';

function ReactHookFormExample() {
const { register, handleSubmit, formState: { errors } } = useForm();

const onSubmit = (data) => {
console.log(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', { required: 'Name is required' })}
placeholder="Name"
/>
{errors.name && <span>{errors.name.message}</span>}

<input
{...register('email', {
required: 'Email is required',
pattern: {
value: /\S+@\S+\.\S+/,
message: 'Invalid email'
}
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}

<button type="submit">Submit</button>
</form>
);
}

Formik

import { Formik, Form, Field, ErrorMessage } from 'formik';

function FormikExample() {
const initialValues = {
name: '',
email: ''
};

const validate = (values) => {
const errors = {};

if (!values.name) {
errors.name = 'Name is required';
}

if (!values.email) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Invalid email';
}

return errors;
};

const onSubmit = (values) => {
console.log(values);
};

return (
<Formik
initialValues={initialValues}
validate={validate}
onSubmit={onSubmit}
>
<Form>
<Field type="text" name="name" placeholder="Name" />
<ErrorMessage name="name" component="div" />

<Field type="email" name="email" placeholder="Email" />
<ErrorMessage name="email" component="div" />

<button type="submit">Submit</button>
</Form>
</Formik>
);
}

Accessibility

ARIA Labels and Descriptions

function AccessibleForm() {
const [errors, setErrors] = useState({});

return (
<form>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
aria-describedby="name-error"
aria-invalid={errors.name ? 'true' : 'false'}
/>
{errors.name && (
<div id="name-error" role="alert">
{errors.name}
</div>
)}
</div>

<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
aria-describedby="email-error"
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<div id="email-error" role="alert">
{errors.email}
</div>
)}
</div>
</form>
);
}

Practice Project: Contact Form

function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);

const validateForm = () => {
const newErrors = {};

if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}

if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}

if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
}

if (!formData.message.trim()) {
newErrors.message = 'Message is required';
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleChange = (event) => {
const { name, value } = event.target;
setFormData(prev => ({ ...prev, [name]: value }));

if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};

const handleSubmit = async (event) => {
event.preventDefault();

if (!validateForm()) return;

setIsSubmitting(true);

try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Form submitted:', formData);
alert('Message sent successfully!');
setFormData({ name: '', email: '', subject: '', message: '' });
} catch (error) {
console.error('Error submitting form:', error);
alert('Error sending message. Please try again.');
} finally {
setIsSubmitting(false);
}
};

return (
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">Name *</label>
<input
id="name"
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className={errors.name ? 'error' : ''}
aria-describedby={errors.name ? 'name-error' : undefined}
aria-invalid={errors.name ? 'true' : 'false'}
/>
{errors.name && (
<div id="name-error" className="error-message" role="alert">
{errors.name}
</div>
)}
</div>

<div className="form-group">
<label htmlFor="email">Email *</label>
<input
id="email"
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
aria-describedby={errors.email ? 'email-error' : undefined}
aria-invalid={errors.email ? 'true' : 'false'}
/>
{errors.email && (
<div id="email-error" className="error-message" role="alert">
{errors.email}
</div>
)}
</div>

<div className="form-group">
<label htmlFor="subject">Subject *</label>
<input
id="subject"
type="text"
name="subject"
value={formData.subject}
onChange={handleChange}
className={errors.subject ? 'error' : ''}
aria-describedby={errors.subject ? 'subject-error' : undefined}
aria-invalid={errors.subject ? 'true' : 'false'}
/>
{errors.subject && (
<div id="subject-error" className="error-message" role="alert">
{errors.subject}
</div>
)}
</div>

<div className="form-group">
<label htmlFor="message">Message *</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="5"
className={errors.message ? 'error' : ''}
aria-describedby={errors.message ? 'message-error' : undefined}
aria-invalid={errors.message ? 'true' : 'false'}
/>
{errors.message && (
<div id="message-error" className="error-message" role="alert">
{errors.message}
</div>
)}
</div>

<button
type="submit"
disabled={isSubmitting}
className="submit-button"
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}

Summary

In this chapter, we covered:

  • Event Handling: Basic events, parameters, and event objects
  • Controlled Components: React-controlled form inputs
  • Uncontrolled Components: Ref-based form access
  • Form Validation: Client-side validation patterns
  • Custom Hooks: Reusable form logic
  • Form Libraries: React Hook Form and Formik
  • Accessibility: ARIA labels and error handling

Next Steps

Additional Resources