Hooks
React Hooks for state management, side effects, performance optimization, and creating custom reusable logic.
Built-in Hooks
useState
import { useState } from 'react';
// Basic state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
</div>
);
}
// Object state
function UserForm() {
const [user, setUser] = useState({ name: '', email: '' });
const updateUser = (field, value) => {
setUser(prev => ({ ...prev, [field]: value }));
};
return (
<form>
<input
value={user.name}
onChange={e => updateUser('name', e.target.value)}
/>
<input
value={user.email}
onChange={e => updateUser('email', e.target.value)}
/>
</form>
);
}
// Array state
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = text => {
setTodos(prev => [...prev, { id: Date.now(), text, completed: false }]);
};
const toggleTodo = id => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = id => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
return (
<div>
<AddTodoForm onAdd={addTodo} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</div>
);
}
useEffect
import { useEffect, useState } from 'react';
// Component mount/unmount
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Empty dependency array = run once
return <div>Timer: {seconds}s</div>;
}
// Data fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetchUser(userId)
.then(userData => {
if (!cancelled) {
setUser(userData);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
// Multiple effects
function Component() {
const [count, setCount] = useState(0);
// Effect for document title
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// Effect for local storage
useEffect(() => {
localStorage.setItem('count', count);
}, [count]);
// Effect for window resize
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Count: {count}</div>;
}
useContext
import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Component using context
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button className={`btn btn-${theme}`} onClick={toggleTheme}>
Toggle Theme (current: {theme})
</button>
);
}
// App structure
function App() {
return (
<ThemeProvider>
<div>
<ThemedButton />
</div>
</ThemeProvider>
);
}
useReducer
import { useReducer } from 'react';
// Action types
const ACTIONS = {
ADD_TODO: 'ADD_TODO',
TOGGLE_TODO: 'TOGGLE_TODO',
DELETE_TODO: 'DELETE_TODO',
};
// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TODO:
return [
...state,
{
id: Date.now(),
text: action.payload,
completed: false,
},
];
case ACTIONS.TOGGLE_TODO:
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case ACTIONS.DELETE_TODO:
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
// Component using reducer
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const addTodo = text => {
dispatch({ type: ACTIONS.ADD_TODO, payload: text });
};
const toggleTodo = id => {
dispatch({ type: ACTIONS.TOGGLE_TODO, payload: id });
};
const deleteTodo = id => {
dispatch({ type: ACTIONS.DELETE_TODO, payload: id });
};
return (
<div>
<AddTodoForm onAdd={addTodo} />
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={() => toggleTodo(todo.id)}
onDelete={() => deleteTodo(todo.id)}
/>
))}
</div>
);
}
useMemo
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items, filter }) {
const [sortOrder, setSortOrder] = useState('asc');
// Memoize expensive calculation
const filteredAndSortedItems = useMemo(() => {
console.log('Calculating filtered and sorted items');
const filtered = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
} else {
return b.name.localeCompare(a.name);
}
});
}, [items, filter, sortOrder]);
return (
<div>
<button
onClick={() => setSortOrder(prev => (prev === 'asc' ? 'desc' : 'asc'))}
>
Sort {sortOrder === 'asc' ? 'Descending' : 'Ascending'}
</button>
<ul>
{filteredAndSortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
useCallback
import { useCallback, useState, memo } from 'react';
// Child component that should not re-render unnecessarily
const ListItem = memo(function ListItem({ item, onDelete }) {
console.log('ListItem rendered for:', item.name);
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
);
});
function ItemList({ items }) {
const [filter, setFilter] = useState('');
// Memoize callback to prevent child re-renders
const handleDelete = useCallback(id => {
console.log('Deleting item:', id);
// Delete logic here
}, []);
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<ul>
{filteredItems.map(item => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</ul>
</div>
);
}
useRef
import { useRef, useEffect, useState } from 'react';
function InputFocus() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current.focus();
}, []);
return (
<div>
<input ref={inputRef} placeholder="This will be focused" />
<button onClick={() => inputRef.current.focus()}>Focus Input</button>
</div>
);
}
// Storing mutable values
function Timer() {
const [time, setTime] = useState(0);
const intervalRef = useRef(null);
const startTimer = () => {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setTime(prev => prev + 1);
}, 1000);
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>Time: {time}s</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
// Previous value tracking
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Custom Hooks
Data Fetching Hook
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(error => {
if (!cancelled) {
setError(error);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
Local Storage Hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use provided 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:', error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists to localStorage
const setValue = value => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error setting localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme ({theme})
</button>
<button onClick={() => setFontSize(fontSize + 2)}>
Increase Font Size ({fontSize}px)
</button>
</div>
);
}
Window Size Hook
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// Usage
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
<p>
Window size: {width} x {height}
</p>
{width < 768 ? <MobileLayout /> : <DesktopLayout />}
</div>
);
}
Debounce 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;
}
// Usage
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { data: results, loading } = useFetch(
debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
{results && (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
)}
</div>
);
}
Form Hook
import { useState } from 'react';
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = name => {
setTouched(prev => ({ ...prev, [name]: true }));
if (validate) {
const fieldErrors = validate({ ...values, [name]: values[name] });
setErrors(prev => ({ ...prev, [name]: fieldErrors[name] || '' }));
}
};
const handleSubmit = onSubmit => e => {
e.preventDefault();
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
onSubmit(values);
}
} else {
onSubmit(values);
}
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset,
};
}
// Usage
function ContactForm() {
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 = 'Email is invalid';
return errors;
};
const form = useForm({ name: '', email: '', message: '' }, validate);
const onSubmit = values => {
console.log('Form submitted:', values);
form.reset();
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input
value={form.values.name}
onChange={e => form.handleChange('name', e.target.value)}
onBlur={() => form.handleBlur('name')}
placeholder="Name"
/>
{form.errors.name && <span>{form.errors.name}</span>}
<input
type="email"
value={form.values.email}
onChange={e => form.handleChange('email', e.target.value)}
onBlur={() => form.handleBlur('email')}
placeholder="Email"
/>
{form.errors.email && <span>{form.errors.email}</span>}
<textarea
value={form.values.message}
onChange={e => form.handleChange('message', e.target.value)}
placeholder="Message"
/>
<button type="submit">Submit</button>
</form>
);
}
Advanced Hook Patterns
Custom Hook with Context
import { createContext, useContext, useReducer } from 'react';
// Auth context and reducer
const AuthContext = createContext();
const authReducer = (state, action) => {
switch (action.type) {
case 'LOGIN':
return { ...state, user: action.payload, isAuthenticated: true };
case 'LOGOUT':
return { ...state, user: null, isAuthenticated: false };
case 'SET_LOADING':
return { ...state, loading: action.payload };
default:
return state;
}
};
// Auth provider
export function AuthProvider({ children }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isAuthenticated: false,
loading: false,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
const { state, dispatch } = context;
const login = async credentials => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const user = await authService.login(credentials);
dispatch({ type: 'LOGIN', payload: user });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
const logout = () => {
dispatch({ type: 'LOGOUT' });
};
return {
user: state.user,
isAuthenticated: state.isAuthenticated,
loading: state.loading,
login,
logout,
};
}
Hook Composition
// Combine multiple hooks
function useUserData(userId) {
const {
data: user,
loading: userLoading,
error: userError,
} = useFetch(`/api/users/${userId}`);
const { data: posts, loading: postsLoading } = useFetch(
`/api/users/${userId}/posts`
);
return {
user,
posts,
loading: userLoading || postsLoading,
error: userError,
};
}
// Hook that depends on other hooks
function useAuthenticatedApi() {
const { user, isAuthenticated } = useAuth();
const authenticatedFetch = useCallback(
async (url, options = {}) => {
if (!isAuthenticated) {
throw new Error('User not authenticated');
}
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${user.token}`,
},
});
},
[isAuthenticated, user]
);
return { authenticatedFetch };
}
Hook Best Practices
Rules of Hooks
// ✅ Good: Always call hooks at the top level
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
// Effect logic
}, []);
return <div>{count}</div>;
}
// ❌ Bad: Conditional hooks
function MyComponent({ condition }) {
if (condition) {
const [count, setCount] = useState(0); // ❌ Don't do this
}
return <div>Component</div>;
}
// ❌ Bad: Hooks in loops
function MyComponent({ items }) {
items.forEach(item => {
const [state, setState] = useState(item); // ❌ Don't do this
});
return <div>Component</div>;
}
Dependency Arrays
// ✅ Good: Include all dependencies
function MyComponent({ userId, filter }) {
const [data, setData] = useState([]);
useEffect(() => {
fetchData(userId, filter).then(setData);
}, [userId, filter]); // Include all dependencies
return <div>{data.length}</div>;
}
// ❌ Bad: Missing dependencies
function MyComponent({ userId, filter }) {
const [data, setData] = useState([]);
useEffect(() => {
fetchData(userId, filter).then(setData);
}, [userId]); // Missing 'filter' dependency
return <div>{data.length}</div>;
}
Custom Hook Design
// ✅ Good: Return object with named properties
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// ❌ Bad: Return array when object would be clearer
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return [count, increment, decrement]; // Less clear
}
Quick Reference
Built-in Hooks
// State
const [state, setState] = useState(initialValue);
// Effects
useEffect(() => {
// Effect logic
return () => {
// Cleanup
};
}, [dependencies]);
// Context
const value = useContext(MyContext);
// Reducer
const [state, dispatch] = useReducer(reducer, initialState);
// Performance
const memoizedValue = useMemo(() => computeValue(), [deps]);
const memoizedCallback = useCallback(() => {}, [deps]);
// Refs
const ref = useRef(initialValue);
Custom Hook Pattern
function useCustomHook(param) {
const [state, setState] = useState(initialValue);
useEffect(() => {
// Effect based on param
}, [param]);
const helper = useCallback(() => {
// Helper function
}, []);
return { state, helper };
}