React Hooks Deep Dive: Complete Guide to useState, useEffect, and Custom Hooks
Master React Hooks with this comprehensive guide covering useState, useEffect, useContext, useReducer, and custom hooks. Learn best practices and advanced patterns.
React Hooks Deep Dive: Complete Guide to useState, useEffect, and Custom Hooks
React Hooks revolutionized how we write React components by allowing us to use state and other React features without writing class components. This comprehensive guide will take you through all the essential hooks and teach you how to create powerful custom hooks.
Introduction to React Hooks
Hooks are functions that let you "hook into" React state and lifecycle features from function components. They were introduced in React 16.8 and have become the preferred way to write React components.
Rules of Hooks
Before diving into specific hooks, it's crucial to understand the rules:
- Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React functions - Call them from React function components or custom hooks
// ❌ Wrong - calling hooks conditionally
function BadExample({ condition }) {
if (condition) {
const [state, setState] = useState(0); // This breaks the rules!
}
return <div>Bad example</div>;
}
// ✅ Correct - hooks called at top level
function GoodExample({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Use the state here instead
}
return <div>Good example</div>;
}
useState Hook
The useState
hook allows you to add state to functional components.
Basic useState Usage
import React, { useState } from 'react';
const CounterExample = () => {
// Declare state variable with initial value
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};
State with Objects
const UserProfile = () => {
const [user, setUser] = useState({
name: '',
email: '',
age: 0,
preferences: {
theme: 'light',
notifications: true,
},
});
const updateName = (newName) => {
setUser((prevUser) => ({
...prevUser,
name: newName,
}));
};
const updatePreferences = (key, value) => {
setUser((prevUser) => ({
...prevUser,
preferences: {
...prevUser.preferences,
[key]: value,
},
}));
};
return (
<div>
<input
type="text"
value={user.name}
onChange={(e) => updateName(e.target.value)}
placeholder="Enter your name"
/>
<label>
<input
type="checkbox"
checked={user.preferences.notifications}
onChange={(e) => updatePreferences('notifications', e.target.checked)}
/>
Enable notifications
</label>
<select
value={user.preferences.theme}
onChange={(e) => updatePreferences('theme', e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
);
};
State with Arrays
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos((prevTodos) => [
...prevTodos,
{
id: Date.now(),
text: inputValue,
completed: false,
},
]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
};
return (
<div>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add Todo</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
Functional State Updates
const Counter = () => {
const [count, setCount] = useState(0);
// ❌ This might not work as expected in rapid clicks
const incrementWrong = () => {
setCount(count + 1);
setCount(count + 1); // This won't increment by 2!
};
// ✅ Use functional updates for predictable behavior
const incrementCorrect = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1); // This will increment by 2
};
const incrementAsync = () => {
setTimeout(() => {
// Always use functional update in async operations
setCount((prevCount) => prevCount + 1);
}, 1000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCorrect}>Increment by 2</button>
<button onClick={incrementAsync}>Increment after 1s</button>
</div>
);
};
useEffect Hook
The useEffect
hook lets you perform side effects in function components. It serves the same purpose as componentDidMount
, componentDidUpdate
, and componentWillUnmount
combined.
Basic useEffect Usage
import React, { useState, useEffect } from 'react';
const BasicEffect = () => {
const [count, setCount] = useState(0);
// Effect runs after every render
useEffect(() => {
document.title = `Count: ${count}`;
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
useEffect with Dependencies
const DataFetcher = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Effect runs only when userId changes
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
};
Cleanup with useEffect
const Timer = () => {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval = null;
if (isActive) {
interval = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
}
// Cleanup function
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isActive]); // Re-run effect when isActive changes
const toggle = () => setIsActive(!isActive);
const reset = () => {
setSeconds(0);
setIsActive(false);
};
return (
<div>
<p>Timer: {seconds}s</p>
<button onClick={toggle}>{isActive ? 'Pause' : 'Start'}</button>
<button onClick={reset}>Reset</button>
</div>
);
};
Multiple useEffect Hooks
const UserDashboard = ({ userId }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
// Effect for fetching user data
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
};
fetchUser();
}, [userId]);
// Effect for fetching user posts
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(`/api/users/${userId}/posts`);
const postsData = await response.json();
setPosts(postsData);
};
fetchPosts();
}, [userId]);
// Effect for setting up notifications WebSocket
useEffect(() => {
const ws = new WebSocket(`ws://localhost:8080/notifications/${userId}`);
ws.onmessage = (event) => {
const notification = JSON.parse(event.data);
setNotifications((prev) => [...prev, notification]);
};
return () => {
ws.close();
};
}, [userId]);
// Effect for document title
useEffect(() => {
if (user) {
document.title = `Dashboard - ${user.name}`;
}
return () => {
document.title = 'App';
};
}, [user]);
return (
<div>
{user && (
<div>
<h1>Welcome, {user.name}!</h1>
<div>Posts: {posts.length}</div>
<div>Notifications: {notifications.length}</div>
</div>
)}
</div>
);
};
useContext Hook
The useContext
hook allows you to consume context values without wrapping components in Consumer components.
Creating and Using Context
import React, { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
const UserContext = createContext();
// Theme Provider Component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
const value = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
};
// User Provider Component
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = async (credentials) => {
// Simulate API call
const userData = await authenticateUser(credentials);
setUser(userData);
setIsAuthenticated(true);
};
const logout = () => {
setUser(null);
setIsAuthenticated(false);
};
const value = {
user,
isAuthenticated,
login,
logout,
};
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
// Custom hooks for consuming context
const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within a UserProvider');
}
return context;
};
// Components using context
const Header = () => {
const { theme, toggleTheme } = useTheme();
const { user, isAuthenticated, logout } = useUser();
return (
<header className={`header ${theme}`}>
<h1>My App</h1>
<div>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
{isAuthenticated ? (
<div>
<span>Welcome, {user.name}!</span>
<button onClick={logout}>Logout</button>
</div>
) : (
<button>Login</button>
)}
</div>
</header>
);
};
const App = () => {
return (
<ThemeProvider>
<UserProvider>
<div className="app">
<Header />
{/* Other components */}
</div>
</UserProvider>
</ThemeProvider>
);
};
Complex Context with useReducer
import React, { createContext, useContext, useReducer } from 'react';
// Shopping Cart Context
const CartContext = createContext();
// Cart actions
const CART_ACTIONS = {
ADD_ITEM: 'ADD_ITEM',
REMOVE_ITEM: 'REMOVE_ITEM',
UPDATE_QUANTITY: 'UPDATE_QUANTITY',
CLEAR_CART: 'CLEAR_CART',
APPLY_DISCOUNT: 'APPLY_DISCOUNT',
};
// Cart reducer
const cartReducer = (state, action) => {
switch (action.type) {
case CART_ACTIONS.ADD_ITEM:
const existingItem = state.items.find(
(item) => item.id === action.payload.id
);
if (existingItem) {
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
),
};
} else {
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }],
};
}
case CART_ACTIONS.REMOVE_ITEM:
return {
...state,
items: state.items.filter((item) => item.id !== action.payload),
};
case CART_ACTIONS.UPDATE_QUANTITY:
return {
...state,
items: state.items.map((item) =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
),
};
case CART_ACTIONS.CLEAR_CART:
return {
...state,
items: [],
};
case CART_ACTIONS.APPLY_DISCOUNT:
return {
...state,
discount: action.payload,
};
default:
return state;
}
};
// Initial state
const initialCartState = {
items: [],
discount: 0,
};
// Cart Provider
const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialCartState);
// Action creators
const addItem = (item) => {
dispatch({ type: CART_ACTIONS.ADD_ITEM, payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: CART_ACTIONS.REMOVE_ITEM, payload: itemId });
};
const updateQuantity = (itemId, quantity) => {
dispatch({
type: CART_ACTIONS.UPDATE_QUANTITY,
payload: { id: itemId, quantity },
});
};
const clearCart = () => {
dispatch({ type: CART_ACTIONS.CLEAR_CART });
};
const applyDiscount = (discount) => {
dispatch({ type: CART_ACTIONS.APPLY_DISCOUNT, payload: discount });
};
// Computed values
const totalItems = state.items.reduce(
(total, item) => total + item.quantity,
0
);
const subtotal = state.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
const total = subtotal - (subtotal * state.discount) / 100;
const value = {
...state,
addItem,
removeItem,
updateQuantity,
clearCart,
applyDiscount,
totalItems,
subtotal,
total,
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
// Custom hook for using cart
const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
// Example usage
const ProductCard = ({ product }) => {
const { addItem } = useCart();
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addItem(product)}>Add to Cart</button>
</div>
);
};
const CartSummary = () => {
const { items, totalItems, total, clearCart } = useCart();
return (
<div className="cart-summary">
<h3>Cart ({totalItems} items)</h3>
{items.map((item) => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
<div>Total: ${total.toFixed(2)}</div>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
};
useReducer Hook
The useReducer
hook is usually preferable to useState
when you have complex state logic or when the next state depends on the previous one.
Basic useReducer Example
import React, { useReducer } from 'react';
// Define action types
const ACTION_TYPES = {
INCREMENT: 'increment',
DECREMENT: 'decrement',
RESET: 'reset',
SET_VALUE: 'set_value',
};
// Reducer function
const counterReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPES.INCREMENT:
return { count: state.count + (action.payload || 1) };
case ACTION_TYPES.DECREMENT:
return { count: state.count - (action.payload || 1) };
case ACTION_TYPES.RESET:
return { count: 0 };
case ACTION_TYPES.SET_VALUE:
return { count: action.payload };
default:
throw new Error(`Unknown action type: ${action.type}`);
}
};
const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT })}>
+1
</button>
<button
onClick={() => dispatch({ type: ACTION_TYPES.INCREMENT, payload: 5 })}
>
+5
</button>
<button onClick={() => dispatch({ type: ACTION_TYPES.DECREMENT })}>
-1
</button>
<button onClick={() => dispatch({ type: ACTION_TYPES.RESET })}>
Reset
</button>
<button
onClick={() => dispatch({ type: ACTION_TYPES.SET_VALUE, payload: 100 })}
>
Set to 100
</button>
</div>
);
};
Complex Form with useReducer
const FORM_ACTIONS = {
UPDATE_FIELD: 'UPDATE_FIELD',
SET_ERRORS: 'SET_ERRORS',
SET_LOADING: 'SET_LOADING',
RESET_FORM: 'RESET_FORM',
SUBMIT_SUCCESS: 'SUBMIT_SUCCESS',
SUBMIT_ERROR: 'SUBMIT_ERROR',
};
const formReducer = (state, action) => {
switch (action.type) {
case FORM_ACTIONS.UPDATE_FIELD:
return {
...state,
values: {
...state.values,
[action.field]: action.value,
},
errors: {
...state.errors,
[action.field]: '', // Clear field error when user types
},
};
case FORM_ACTIONS.SET_ERRORS:
return {
...state,
errors: action.errors,
};
case FORM_ACTIONS.SET_LOADING:
return {
...state,
isLoading: action.isLoading,
};
case FORM_ACTIONS.RESET_FORM:
return {
...state,
values: action.initialValues || state.initialValues,
errors: {},
isLoading: false,
};
case FORM_ACTIONS.SUBMIT_SUCCESS:
return {
...state,
isLoading: false,
errors: {},
submitCount: state.submitCount + 1,
};
case FORM_ACTIONS.SUBMIT_ERROR:
return {
...state,
isLoading: false,
errors: action.errors,
};
default:
return state;
}
};
const ContactForm = () => {
const initialValues = {
name: '',
email: '',
subject: '',
message: '',
};
const [state, dispatch] = useReducer(formReducer, {
values: initialValues,
initialValues,
errors: {},
isLoading: false,
submitCount: 0,
});
const updateField = (field, value) => {
dispatch({
type: FORM_ACTIONS.UPDATE_FIELD,
field,
value,
});
};
const validateForm = () => {
const errors = {};
if (!state.values.name.trim()) {
errors.name = 'Name is required';
}
if (!state.values.email.trim()) {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(state.values.email)) {
errors.email = 'Email is invalid';
}
if (!state.values.subject.trim()) {
errors.subject = 'Subject is required';
}
if (!state.values.message.trim()) {
errors.message = 'Message is required';
}
return errors;
};
const handleSubmit = async (e) => {
e.preventDefault();
const errors = validateForm();
if (Object.keys(errors).length > 0) {
dispatch({ type: FORM_ACTIONS.SET_ERRORS, errors });
return;
}
dispatch({ type: FORM_ACTIONS.SET_LOADING, isLoading: true });
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
dispatch({ type: FORM_ACTIONS.SUBMIT_SUCCESS });
alert('Form submitted successfully!');
// Reset form after successful submission
dispatch({ type: FORM_ACTIONS.RESET_FORM });
} catch (error) {
dispatch({
type: FORM_ACTIONS.SUBMIT_ERROR,
errors: { submit: 'Failed to submit form. Please try again.' },
});
}
};
const resetForm = () => {
dispatch({ type: FORM_ACTIONS.RESET_FORM });
};
return (
<form onSubmit={handleSubmit} className="contact-form">
<h2>Contact Us</h2>
<div className="form-group">
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
value={state.values.name}
onChange={(e) => updateField('name', e.target.value)}
className={state.errors.name ? 'error' : ''}
/>
{state.errors.name && (
<span className="error-text">{state.errors.name}</span>
)}
</div>
<div className="form-group">
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
value={state.values.email}
onChange={(e) => updateField('email', e.target.value)}
className={state.errors.email ? 'error' : ''}
/>
{state.errors.email && (
<span className="error-text">{state.errors.email}</span>
)}
</div>
<div className="form-group">
<label htmlFor="subject">Subject:</label>
<input
type="text"
id="subject"
value={state.values.subject}
onChange={(e) => updateField('subject', e.target.value)}
className={state.errors.subject ? 'error' : ''}
/>
{state.errors.subject && (
<span className="error-text">{state.errors.subject}</span>
)}
</div>
<div className="form-group">
<label htmlFor="message">Message:</label>
<textarea
id="message"
rows="5"
value={state.values.message}
onChange={(e) => updateField('message', e.target.value)}
className={state.errors.message ? 'error' : ''}
/>
{state.errors.message && (
<span className="error-text">{state.errors.message}</span>
)}
</div>
{state.errors.submit && (
<div className="error-text">{state.errors.submit}</div>
)}
<div className="form-actions">
<button type="submit" disabled={state.isLoading}>
{state.isLoading ? 'Submitting...' : 'Submit'}
</button>
<button type="button" onClick={resetForm}>
Reset
</button>
</div>
<div className="form-info">Submit count: {state.submitCount}</div>
</form>
);
};
Custom Hooks
Custom hooks allow you to extract component logic into reusable functions. They are JavaScript functions that start with "use" and can call other hooks.
Simple Custom Hooks
// useToggle hook
const useToggle = (initialValue = false) => {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue((prev) => !prev), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, toggle, setTrue, setFalse];
};
// useCounter hook
const useCounter = (initialValue = 0, step = 1) => {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((prev) => prev + step), [step]);
const decrement = useCallback(() => setCount((prev) => prev - step), [step]);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
const setValue = useCallback((value) => setCount(value), []);
return {
count,
increment,
decrement,
reset,
setValue,
};
};
// useLocalStorage hook
const useLocalStorage = (key, initialValue) => {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback(
(value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
return [storedValue, setValue];
};
// Usage examples
const ExampleComponent = () => {
const [isVisible, toggleVisible, showElement, hideElement] = useToggle(false);
const { count, increment, decrement, reset } = useCounter(0, 2);
const [name, setName] = useLocalStorage('userName', '');
return (
<div>
<button onClick={toggleVisible}>{isVisible ? 'Hide' : 'Show'}</button>
{isVisible && <p>This element is toggleable!</p>}
<div>
<p>Count: {count}</p>
<button onClick={increment}>+2</button>
<button onClick={decrement}>-2</button>
<button onClick={reset}>Reset</button>
</div>
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Stored name: {name}</p>
</div>
</div>
);
};
Advanced Custom Hooks
// useFetch hook for data fetching
const useFetch = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [url, JSON.stringify(options)]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch };
};
// useDebounce hook
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
// useWindowSize hook
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
handleResize(); // Call handler right away so state gets updated with initial window size
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
};
// useIntersectionObserver hook
const useIntersectionObserver = (elementRef, options = {}) => {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(element);
return () => {
observer.unobserve(element);
};
}, [elementRef, options]);
return isIntersecting;
};
// Complex custom hook: useForm
const useForm = (initialValues, validationRules = {}) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const setValue = useCallback(
(name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }));
}
},
[errors]
);
const setFieldTouched = useCallback((name) => {
setTouched((prev) => ({ ...prev, [name]: true }));
}, []);
const validate = useCallback(() => {
const newErrors = {};
Object.keys(validationRules).forEach((field) => {
const rules = validationRules[field];
const value = values[field];
for (const rule of rules) {
const error = rule(value, values);
if (error) {
newErrors[field] = error;
break; // Stop at first error for this field
}
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [values, validationRules]);
const handleSubmit = useCallback(
(onSubmit) => {
return async (e) => {
e.preventDefault();
// Mark all fields as touched
const allTouched = Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {});
setTouched(allTouched);
if (!validate()) return;
setIsSubmitting(true);
try {
await onSubmit(values);
} catch (error) {
console.error('Form submission error:', error);
} finally {
setIsSubmitting(false);
}
};
},
[values, validate]
);
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
setIsSubmitting(false);
}, [initialValues]);
const getFieldProps = useCallback(
(name) => ({
value: values[name] || '',
onChange: (e) => setValue(name, e.target.value),
onBlur: () => setFieldTouched(name),
error: touched[name] && errors[name],
}),
[values, errors, touched, setValue, setFieldTouched]
);
return {
values,
errors,
touched,
isSubmitting,
setValue,
setFieldTouched,
handleSubmit,
resetForm,
getFieldProps,
isValid: Object.keys(errors).length === 0,
};
};
// Validation rules
const required =
(message = 'This field is required') =>
(value) => {
if (!value || !value.toString().trim()) {
return message;
}
};
const email =
(message = 'Please enter a valid email') =>
(value) => {
if (value && !/\S+@\S+\.\S+/.test(value)) {
return message;
}
};
const minLength = (min, message) => (value) => {
if (value && value.length < min) {
return message || `Must be at least ${min} characters`;
}
};
// Usage of advanced custom hooks
const AdvancedExample = () => {
const { data: users, loading, error, refetch } = useFetch('/api/users');
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const windowSize = useWindowSize();
const lazyElementRef = useRef();
const isElementVisible = useIntersectionObserver(lazyElementRef, {
threshold: 0.1,
});
const form = useForm(
{ name: '', email: '', message: '' },
{
name: [required()],
email: [required(), email()],
message: [required(), minLength(10)],
}
);
const handleFormSubmit = async (values) => {
console.log('Submitting:', values);
await new Promise((resolve) => setTimeout(resolve, 1000));
alert('Form submitted successfully!');
form.resetForm();
};
// Effect for debounced search
useEffect(() => {
if (debouncedSearchTerm) {
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<div>
<div>
<h3>Window Size</h3>
<p>
Width: {windowSize.width}px, Height: {windowSize.height}px
</p>
</div>
<div>
<h3>Search</h3>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search users..."
/>
<p>Debounced: {debouncedSearchTerm}</p>
</div>
<div>
<h3>Users</h3>
{loading && <p>Loading...</p>}
{error && <p>Error: {error}</p>}
{data && (
<div>
<button onClick={refetch}>Refresh</button>
<ul>
{users.slice(0, 5).map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)}
</div>
<form onSubmit={form.handleSubmit(handleFormSubmit)}>
<h3>Contact Form</h3>
<div>
<input {...form.getFieldProps('name')} placeholder="Name" />
{form.getFieldProps('name').error && (
<span className="error">{form.getFieldProps('name').error}</span>
)}
</div>
<div>
<input {...form.getFieldProps('email')} placeholder="Email" />
{form.getFieldProps('email').error && (
<span className="error">{form.getFieldProps('email').error}</span>
)}
</div>
<div>
<textarea {...form.getFieldProps('message')} placeholder="Message" />
{form.getFieldProps('message').error && (
<span className="error">{form.getFieldProps('message').error}</span>
)}
</div>
<button type="submit" disabled={form.isSubmitting || !form.isValid}>
{form.isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
<div style={{ height: '1000px', background: '#f0f0f0', padding: '20px' }}>
<p>Scroll down to see the lazy element...</p>
</div>
<div
ref={lazyElementRef}
style={{ height: '200px', background: '#e0e0e0' }}
>
<h3>Lazy Element</h3>
<p>Is visible: {isElementVisible ? 'Yes' : 'No'}</p>
</div>
</div>
);
};
Best Practices and Common Patterns
Optimizing Hook Performance
import React, { useState, useEffect, useCallback, useMemo } from 'react';
const OptimizedComponent = ({ items, category, onItemSelect }) => {
const [searchTerm, setSearchTerm] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
// Memoize expensive calculations
const filteredAndSortedItems = useMemo(() => {
const filtered = items.filter(
(item) =>
item.category === category &&
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return filtered.sort((a, b) => {
const multiplier = sortOrder === 'asc' ? 1 : -1;
return a.name.localeCompare(b.name) * multiplier;
});
}, [items, category, searchTerm, sortOrder]);
// Memoize event handlers to prevent unnecessary re-renders
const handleSearchChange = useCallback((e) => {
setSearchTerm(e.target.value);
}, []);
const handleSortChange = useCallback((newSortOrder) => {
setSortOrder(newSortOrder);
}, []);
const handleItemClick = useCallback(
(item) => {
onItemSelect(item);
},
[onItemSelect]
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search items..."
/>
<button onClick={() => handleSortChange('asc')}>Sort A-Z</button>
<button onClick={() => handleSortChange('desc')}>Sort Z-A</button>
<ul>
{filteredAndSortedItems.map((item) => (
<li key={item.id} onClick={() => handleItemClick(item)}>
{item.name}
</li>
))}
</ul>
</div>
);
};
Error Boundaries with Hooks
import React, { useState, useEffect } from 'react';
// Custom hook for error handling
const useErrorHandler = () => {
const [error, setError] = useState(null);
const resetError = () => setError(null);
const handleError = (error) => {
setError(error);
console.error('Error caught by error handler:', error);
};
return { error, resetError, handleError };
};
// Component with error handling
const DataComponent = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const { error, resetError, handleError } = useErrorHandler();
const fetchData = async () => {
setLoading(true);
resetError();
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
if (error) {
return (
<div className="error-container">
<h3>Something went wrong</h3>
<p>{error.message}</p>
<button onClick={fetchData}>Try Again</button>
</div>
);
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>No data available</p>
)}
</div>
);
};
Conclusion
React Hooks have revolutionized how we write React components, making it easier to share logic between components and manage state in functional components. Understanding these core hooks and how to create custom hooks will significantly improve your React development skills.
Key Takeaways
- useState: Perfect for local component state management
- useEffect: Handle side effects and lifecycle events
- useContext: Share state across component trees without prop drilling
- useReducer: Manage complex state logic with predictable updates
- Custom Hooks: Extract and reuse component logic across multiple components
Best Practices
- Always follow the Rules of Hooks
- Use the dependency array in useEffect wisely
- Memoize expensive calculations with useMemo
- Memoize event handlers with useCallback
- Create custom hooks for reusable logic
- Keep components small and focused
- Use TypeScript for better type safety
Next Steps
Now that you've mastered React Hooks, consider exploring:
- Advanced React Patterns: Render props, compound components, higher-order components
- State Management Libraries: Redux Toolkit, Zustand, Jotai
- Testing Hooks: React Testing Library, Jest
- Performance Optimization: React.memo, React Profiler
- Concurrent Features: Suspense, useDeferredValue, useTransition
React Hooks provide a powerful and elegant way to manage state and side effects in functional components. With practice and understanding of these patterns, you'll be able to build robust, maintainable React applications!