Skip to main content

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 };
}