Hooks
13 min read- React Hooks - Complete Guide
- Table of Contents
- useState
- Basic Usage
- Functional Updates
- Lazy Initialization
- Multiple State Variables
- useEffect
- Basic Side Effects
- Cleanup Functions
- Common Mistakes
- Async Effects
- useContext
- Basic Context Usage
- Complex Context with Actions
- useReducer
- Basic Reducer
- Complex State Management
- useCallback
- Preventing Unnecessary Re-renders
- useMemo
- Expensive Calculations
- useRef
- Accessing DOM Elements
- Storing Mutable Values
- useImperativeHandle
- Exposing Custom Instance Methods
- useLayoutEffect
- Synchronous DOM Mutations
- Custom Hooks
- Data Fetching Hook
- Local Storage Hook
- Debounce Hook
- Rules of Hooks
- The Two Rules
- Questions & Answers
React Hooks - Complete Guide
Master React Hooks from basics to advanced patterns for senior-level interviews.
Table of Contents
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- Custom Hooks
- Rules of Hooks
useState
Basic Usage
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>
Increment (functional)
</button>
</div>
);
}
Functional Updates
// ❌ Bad - stale closure problem
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
setCount(count + 1); // Still only increments by 1
};
return <button onClick={increment}>+2</button>;
}
// ✅ Good - functional updates
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Correctly increments by 2
};
return <button onClick={increment}>+2</button>;
}
Lazy Initialization
// ❌ Bad - expensive computation runs on every render
function ExpensiveComponent() {
const [data, setData] = useState(computeExpensiveValue());
// computeExpensiveValue() runs on every render
}
// ✅ Good - lazy initialization runs only once
function ExpensiveComponent() {
const [data, setData] = useState(() => computeExpensiveValue());
// computeExpensiveValue() runs only on initial render
}
Multiple State Variables
function UserProfile() {
// ❌ Bad - single object state
const [state, setState] = useState({
name: '',
email: '',
age: 0
});
const updateName = (name) => {
setState({ ...state, name }); // Easy to forget spreading
};
// ✅ Good - separate state for independent values
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
}
useEffect
Basic Side Effects
function DataFetcher({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
Cleanup Functions
function ChatRoom({ roomId }) {
useEffect(() => {
const socket = connectToRoom(roomId);
// Cleanup runs when component unmounts or roomId changes
return () => {
socket.disconnect();
};
}, [roomId]);
return <div>Chat Room: {roomId}</div>;
}
// Event listeners cleanup
function WindowSize() {
const [size, setSize] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setSize(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array = run once on mount
return <div>Width: {size}px</div>;
}
Common Mistakes
// ❌ Bad - missing dependency
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(setResults);
}, []); // Missing 'query' dependency!
return <div>{results.length} results</div>;
}
// ✅ Good - all dependencies included
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(setResults);
}, [query]);
return <div>{results.length} results</div>;
}
// ❌ Bad - infinite loop
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Causes infinite loop!
}); // No dependency array = runs after every render
return <div>{count}</div>;
}
Async Effects
// ❌ Bad - async useEffect
useEffect(async () => {
const data = await fetchData(); // Don't do this
setData(data);
}, []);
// ✅ Good - async function inside
useEffect(() => {
async function loadData() {
const data = await fetchData();
setData(data);
}
loadData();
}, []);
// ✅ Better - with cleanup for race conditions
useEffect(() => {
let cancelled = false;
async function loadData() {
const data = await fetchData();
if (!cancelled) {
setData(data);
}
}
loadData();
return () => {
cancelled = true;
};
}, []);
// ✅ Best - with AbortController
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort();
}, []);
useContext
Basic Context Usage
import { createContext, useContext } from 'react';
// Create context
const ThemeContext = createContext('light');
// Provider
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
// Consumer
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={`btn-${theme}`}>
Themed Button
</button>
);
}
Complex Context with Actions
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const login = async (credentials) => {
const user = await api.login(credentials);
setUser(user);
};
const logout = () => {
setUser(null);
};
const value = {
user,
login,
logout,
isAuthenticated: !!user
};
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
}
// Custom hook for easier usage
function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
}
// Usage
function Profile() {
const { user, logout } = useUser();
return (
<div>
<h1>{user.name}</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
useReducer
Basic Reducer
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Complex State Management
const initialState = {
items: [],
loading: false,
error: null
};
function todoReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'ADD_TODO':
return { ...state, items: [...state.items, action.payload] };
case 'REMOVE_TODO':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'TOGGLE_TODO':
return {
...state,
items: state.items.map(item =>
item.id === action.payload
? { ...item, completed: !item.completed }
: item
)
};
default:
return state;
}
}
function TodoList() {
const [state, dispatch] = useReducer(todoReducer, initialState);
useEffect(() => {
dispatch({ type: 'FETCH_START' });
fetch('/api/todos')
.then(res => res.json())
.then(data => dispatch({ type: 'FETCH_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'FETCH_ERROR', payload: err.message }));
}, []);
const addTodo = (text) => {
const newTodo = { id: Date.now(), text, completed: false };
dispatch({ type: 'ADD_TODO', payload: newTodo });
};
return (
<div>
{state.loading && <div>Loading...</div>}
{state.error && <div>Error: {state.error}</div>}
<ul>
{state.items.map(item => (
<li key={item.id}>
<input
type="checkbox"
checked={item.completed}
onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: item.id })}
/>
{item.text}
<button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: item.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
useCallback
Preventing Unnecessary Re-renders
import { useState, useCallback, memo } from 'react';
// Child component that should only re-render when onClick changes
const ExpensiveChild = memo(({ onClick, data }) => {
console.log('ExpensiveChild rendered');
return <button onClick={onClick}>{data}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// ❌ Bad - new function created on every render
const handleClick = () => {
console.log('Clicked!');
};
// ✅ Good - function reference stays the same
const handleClickMemoized = useCallback(() => {
console.log('Clicked!');
}, []); // Empty deps = never changes
// ✅ Good - function updates when count changes
const handleIncrement = useCallback(() => {
setCount(count + 1);
}, [count]);
// ✅ Better - functional update doesn't need dependency
const handleIncrementBetter = useCallback(() => {
setCount(c => c + 1);
}, []); // Can stay empty with functional update
return (
<div>
<p>Count: {count}</p>
<p>Other: {other}</p>
<button onClick={() => setOther(other + 1)}>Change Other</button>
<ExpensiveChild onClick={handleClickMemoized} data="Click me" />
</div>
);
}
useMemo
Expensive Calculations
import { useMemo } from 'react';
function DataGrid({ items, sortBy }) {
// ❌ Bad - sorts on every render
const sortedItems = items.sort((a, b) =>
a[sortBy] > b[sortBy] ? 1 : -1
);
// ✅ Good - only re-sorts when items or sortBy changes
const sortedItems = useMemo(() => {
console.log('Sorting items...');
return items.sort((a, b) =>
a[sortBy] > b[sortBy] ? 1 : -1
);
}, [items, sortBy]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// Memoizing objects to prevent reference changes
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ❌ Bad - new object every render
const config = { userId, settings: { theme: 'dark' } };
// ✅ Good - same object reference unless userId changes
const config = useMemo(() => ({
userId,
settings: { theme: 'dark' }
}), [userId]);
useEffect(() => {
fetchUser(config).then(setUser);
}, [config]); // Won't trigger unnecessarily
return <div>{user?.name}</div>;
}
useRef
Accessing DOM Elements
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
Storing Mutable Values
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
// Tracking previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useImperativeHandle
Exposing Custom Instance Methods
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyInputRef = useRef();
const handleSubmit = () => {
const value = fancyInputRef.current.getValue();
console.log('Value:', value);
fancyInputRef.current.clear();
};
return (
<div>
<FancyInput ref={fancyInputRef} />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
useLayoutEffect
Synchronous DOM Mutations
import { useLayoutEffect, useRef, useState } from 'react';
// Measuring DOM before paint
function Tooltip({ children }) {
const [tooltipHeight, setTooltipHeight] = useState(0);
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const { height } = tooltipRef.current.getBoundingClientRect();
setTooltipHeight(height);
}, [children]);
return (
<div
ref={tooltipRef}
style={{ transform: `translateY(-${tooltipHeight}px)` }}
>
{children}
</div>
);
}
// useEffect vs useLayoutEffect
function Component() {
const [show, setShow] = useState(false);
// ❌ useEffect - may cause flicker
useEffect(() => {
if (show) {
// DOM mutation after paint
element.style.color = 'red';
}
}, [show]);
// ✅ useLayoutEffect - no flicker
useLayoutEffect(() => {
if (show) {
// DOM mutation before paint
element.style.color = 'red';
}
}, [show]);
}
Custom Hooks
Data Fetching Hook
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(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err.message);
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}</div>;
return <div>{user.name}</div>;
}
Local Storage Hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme ({theme})
</button>
);
}
Debounce Hook
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 SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// API call only after user stops typing for 500ms
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Rules of Hooks
The Two Rules
- Only call hooks at the top level
- Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React functions
- React function components
- Custom hooks
// ❌ Bad - conditional hook
function Component({ shouldFetch }) {
if (shouldFetch) {
const [data, setData] = useState(null); // DON'T DO THIS
}
}
// ✅ Good - hook at top level
function Component({ shouldFetch }) {
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) {
fetchData().then(setData);
}
}, [shouldFetch]);
}
// ❌ Bad - hook in loop
function Component({ items }) {
return items.map(item => {
const [selected, setSelected] = useState(false); // DON'T DO THIS
return <div>{item}</div>;
});
}
// ✅ Good - separate component
function Item({ item }) {
const [selected, setSelected] = useState(false);
return <div>{item}</div>;
}
function Component({ items }) {
return items.map(item => <Item key={item.id} item={item} />);
}
Questions & Answers
A: Use useCallback to memoize functions, useMemo to memoize values. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
A: useEffect runs after paint (asynchronous), useLayoutEffect runs before paint (synchronous). Use useLayoutEffect when you need to measure/mutate DOM before browser paints.
A: Use useReducer when you have complex state logic with multiple sub-values, when next state depends on previous state, or when you want to optimize performance by passing dispatch down instead of callbacks.
A: Create an async function inside the effect and call it, or use .then(). Never make the effect callback itself async. Always handle cleanup for race conditions.
A: Missing dependency array runs effect after every render. Including objects/arrays created during render as dependencies causes new references every render. Use useMemo or move object creation inside the effect.
A: When you have reusable stateful logic used in multiple components, complex logic that clutters components, or when you want to encapsulate related hooks together.
A: No, hooks must be called in the same order every render. Dynamic hook calls break React's internal tracking. Create separate components instead.
A: Return a cleanup function from the effect. It runs before the effect re-runs and when component unmounts.
A: Store mutable values that don't trigger re-renders when changed, access DOM elements, keep instance variables between renders, and track previous values.
A: Use React.memo for components, useCallback for functions, useMemo for expensive calculations, split state to prevent unnecessary updates, and lift state only when needed.