React Hooks and State Management
React Hooks revolutionized how we write React components. They let you use state and other React features without writing a class. This guide covers everything you need to know about modern React development.
What are React Hooks?
Hooks are functions that let you “hook into” React features from function components. They were introduced in React 16.8 and solve common problems like wrapper hell and complex component logic.
Why Hooks Matter
Before hooks, stateful components required classes:
// Old way with classes
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+</button>
</div>
);
}
}With hooks, same logic becomes much cleaner:
// Modern way with hooks
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
</div>
);
}Core Hooks
useState Hook
Manage state in function components:
import { useState } from 'react';
function TodoApp() {
// Array destructuring: [currentValue, updateFunction]
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
const addTodo = () => {
if (input.trim()) {
setTodos([...todos, {
id: Date.now(),
text: input,
completed: false
}]);
setInput('');
}
};
return (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Add a todo..."
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}useReducer Hook
Manage complex state with actions:
import { useReducer } from 'react';
const initialState = {
todos: [],
filter: 'all' // 'all', 'active', 'completed'
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, action.payload]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
}
function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({
type: 'ADD_TODO',
payload: {
id: Date.now(),
text,
completed: false
}
});
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const setFilter = (filter) => {
dispatch({ type: 'SET_FILTER', payload: filter });
};
const filteredTodos = state.todos.filter(todo => {
if (state.filter === 'active') return !todo.completed;
if (state.filter === 'completed') return todo.completed;
return true;
});
return (
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
<ul>
{filteredTodos.map(todo => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
>
{todo.text}
</li>
))}
</ul>
</div>
);
}Effect Hooks
useEffect Hook
Handle side effects like API calls and subscriptions:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (isMounted) {
setUser(userData);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setUser(null);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchUser();
// Cleanup function
return () => {
isMounted = false;
};
}, [userId]); // Dependency array - re-run when userId changes
if (loading) return <div>Loading user...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}useEffect for Multiple Effects
import { useState, useEffect } from 'react';
function App() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const [onlineStatus, setOnlineStatus] = useState(navigator.onLine);
// Handle window resize
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array means run once
// Handle online/offline status
useEffect(() => {
const handleOnline = () => setOnlineStatus(true);
const handleOffline = () => setOnlineStatus(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return (
<div>
<p>Window: {windowSize.width} x {windowSize.height}</p>
<p>Status: {onlineStatus ? 'Online' : 'Offline'}</p>
</div>
);
}Custom Hooks
Create reusable logic with custom hooks:
useLocalStorage Hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Update localStorage when value changes
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Using the custom hook
function App() {
const [name, setName] = useLocalStorage('name', '');
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div className={`app ${theme}`}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<h1>Hello, {name || 'Guest'}!</h1>
</div>
);
}useDebounce Hook
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Using debounce for search
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
console.log('Searching for:', debouncedSearchTerm);
// Make API call here
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<p>Searching for: {debouncedSearchTerm}</p>
</div>
);
}Advanced Hooks
useContext Hook
Share state between components without prop drilling:
import { createContext, useContext } from 'react';
// Create context
const ThemeContext = createContext();
// Theme provider component
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(currentTheme => currentTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<Toolbar />
<Content />
</ThemeContext.Provider>
);
}
// Consumer component using context
function Toolbar() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div className={`toolbar ${theme}`}>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
</div>
);
}
function Content() {
const { theme } = useContext(ThemeContext);
return (
<div className={`content ${theme}`}>
<p>This content uses the {theme} theme.</p>
</div>
);
}useRef Hook
Access DOM elements and store mutable values:
import { useRef, useEffect } from 'react';
function TextInputWithFocus() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
useEffect(() => {
// Focus input on component mount
inputRef.current.focus();
}, []);
return (
<div>
<input ref={inputRef} type="text" placeholder="Click button to focus" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
// Using useRef for previous value
function PreviousValueExample() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount || 'N/A'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}State Management Patterns
Multiple useState vs useReducer
Choose based on complexity:
// Simple state - use useState
function SimpleForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
// Good for few independent values
return <form>...</form>;
}
// Complex state - use useReducer
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, {
user: { name: '', email: '', age: '' },
validation: { errors: [], isValid: false },
ui: { isSubmitting: false, showSuccess: false }
});
// Better for related state values
return <form>...</form>;
}Lifting State Up
Share state between sibling components:
function Parent() {
const [sharedData, setSharedData] = useState('');
return (
<div>
<ChildA
data={sharedData}
onDataChange={setSharedData}
/>
<ChildB data={sharedData} />
</div>
);
}
function ChildA({ data, onDataChange }) {
return (
<input
value={data}
onChange={(e) => onDataChange(e.target.value)}
placeholder="Type something..."
/>
);
}
function ChildB({ data }) {
return <p>Data from sibling: {data}</p>;
}Performance Optimization
useMemo Hook
Cache expensive calculations:
import { useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
const expensiveCalculation = useMemo(() => {
console.log('Running expensive calculation...');
return items
.filter(item => item.name.toLowerCase().includes(filter.toLowerCase()))
.reduce((sum, item) => sum + item.price, 0);
}, [items, filter]); // Re-calculate only when items or filter changes
return (
<div>
<p>Total: ${expensiveCalculation}</p>
</div>
);
}useCallback Hook
Cache function references:
import { useState, useCallback } from 'react';
function TodoList({ todos }) {
const [newTodo, setNewTodo] = useState('');
// Function won't be recreated on every render
const addTodo = useCallback((text) => {
if (text.trim()) {
// Add todo logic here
console.log('Adding:', text);
}
}, []); // Empty dependency array - function never changes
const handleSubmit = useCallback((e) => {
e.preventDefault();
addTodo(newTodo);
setNewTodo('');
}, [newTodo, addTodo]);
return (
<form onSubmit={handleSubmit}>
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add todo..."
/>
<button type="submit">Add</button>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onUpdate={addTodo} // Stable function reference
/>
))}
</form>
);
}Complete Example: Shopping Cart
import { useState, useReducer, useContext, createContext } from 'react';
// Cart context
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_TO_CART':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_FROM_CART':
const item = state.items.find(item => item.id === action.payload);
return {
...state,
items: state.items.filter(item => item.id !== action.payload),
total: state.total - item.price
};
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
}
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
const addToCart = (product) => {
dispatch({ type: 'ADD_TO_CART', payload: product });
};
const removeFromCart = (productId) => {
dispatch({ type: 'REMOVE_FROM_CART', payload: productId });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const value = { cart, addToCart, removeFromCart, clearCart };
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
function ProductList() {
const { addToCart } = useContext(CartContext);
const products = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Keyboard', price: 79 }
];
return (
<div>
<h2>Products</h2>
{products.map(product => (
<div key={product.id} className="product">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>
Add to Cart
</button>
</div>
))}
</div>
);
}
function ShoppingCart() {
const { cart, removeFromCart, clearCart } = useContext(CartContext);
return (
<div>
<h2>Shopping Cart ({cart.items.length} items)</h2>
{cart.items.map(item => (
<div key={item.id} className="cart-item">
<span>{item.name} - ${item.price}</span>
<button onClick={() => removeFromCart(item.id)}>
Remove
</button>
</div>
))}
<div className="cart-total">
Total: ${cart.total}
</div>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
function App() {
return (
<CartProvider>
<div>
<ProductList />
<ShoppingCart />
</div>
</CartProvider>
);
}Best Practices
Rules of Hooks
- Only call hooks at the top level - Never inside loops, conditions, or nested functions
- Only call hooks from React functions - Never from regular JavaScript functions
- Custom hooks must start with “use” - Follow the naming convention
// Good
function MyComponent() {
const [state, setState] = useState(initialState);
// ... rest of component
}
// Bad - hook inside condition
function MyComponent() {
if (someCondition) {
const [state, setState] = useState(initialState); // ❌
}
}
// Good - extract custom hook
function useCustomCondition(someCondition) {
const [state, setState] = useState(initialState);
return [state, setState];
}Performance Tips
// Use useMemo for expensive calculations
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]);
// Use useCallback for event handlers
const handleClick = useCallback((id) => {
doSomething(id);
}, [doSomething]);
// Split state when possible
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// Better than:
const [formData, setFormData] = useState({ name: '', email: '' });External Resources:
Related Tutorials: