React Hooks fundamentally changed how we write React components. Since their introduction in React 16.8, hooks have become the standard way to manage state, side effects, and shared logic in functional components. This complete guide covers every important hook with real-world examples and 2026 best practices.
useState: Managing Component State
useState is the most fundamental Hook. It returns a stateful value and a function to update it.
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(prev => prev - 1)}>Decrement</button>
</div>
);
}
// Lazy initialization — runs only on first render
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data') || '{}');
});
// Updating objects — always create a new object
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, name: 'Alice' }));
// Updating arrays — use spread or filter/map
const [items, setItems] = useState<string[]>([]);
setItems(prev => [...prev, 'new item']);
setItems(prev => prev.filter(item => item !== 'remove me'));useEffect: Side Effects and Lifecycle
useEffect lets you perform side effects in function components. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
// Runs when userId changes (componentDidMount + componentDidUpdate)
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setUser(data);
});
// Cleanup function (componentWillUnmount)
return () => { cancelled = true; };
}, [userId]); // dependency array
return <div>{user?.name}</div>;
}
// Run once on mount
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []); // empty dependency array
// Run on every render (rarely needed)
useEffect(() => {
console.log('Component rendered');
}); // no dependency arrayuseContext: Consuming Context
useContext lets you subscribe to React context without nesting Consumer components. It simplifies passing data deeply through the component tree.
import { createContext, useContext, useState } from 'react';
// 1. Create a context with a default value
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | null>(null);
// 2. Create a provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 3. Consume context with useContext
function ThemeButton() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('Must be inside ThemeProvider');
return (
<button onClick={ctx.toggleTheme}>
Current: {ctx.theme}
</button>
);
}
// 4. Custom hook for cleaner usage
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
return ctx;
}useMemo: Memoizing Expensive Computations
useMemo caches the result of an expensive calculation between re-renders. Only recomputes when dependencies change.
import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// Only recalculates when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize a sorted copy
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<ul>
{sortedItems.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// Do NOT overuse useMemo — only for truly expensive computations
// Simple operations do not need memoization
const total = useMemo(() => items.reduce((sum, i) => sum + i.price, 0), [items]);useCallback: Memoizing Functions
useCallback returns a memoized version of a callback function. It is useful when passing callbacks to optimized child components that rely on reference equality.
import { useCallback, useState, memo } from 'react';
// Child component wrapped in memo — only re-renders if props change
const SearchInput = memo(({ onSearch }: { onSearch: (q: string) => void }) => {
console.log('SearchInput rendered');
return <input onChange={e => onSearch(e.target.value)} />;
});
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
// Without useCallback, a new function is created every render
// causing SearchInput to re-render unnecessarily
const handleSearch = useCallback((q: string) => {
setQuery(q);
fetch(`/api/search?q=${q}`)
.then(res => res.json())
.then(setResults);
}, []); // stable reference
return (
<div>
<SearchInput onSearch={handleSearch} />
<ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>
</div>
);
}useRef: Mutable References
useRef returns a mutable ref object whose .current property persists across renders without causing re-renders.
import { useRef, useEffect, useState } from 'react';
function TextInputWithFocus() {
// DOM reference
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus(); // auto-focus on mount
}, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
function StopWatch() {
const [time, setTime] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
// Track previous value
const prevTimeRef = useRef(time);
useEffect(() => { prevTimeRef.current = time; });
return (
<div>
<p>Time: {time}s (prev: {prevTimeRef.current}s)</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}useReducer: Complex State Logic
useReducer is an alternative to useState for managing complex state logic. It follows the Redux pattern of dispatching actions to a reducer function.
import { useReducer } from 'react';
interface State {
count: number;
step: number;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return { count: 0, step: 1 };
case 'setStep':
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
return (
<div>
<p>Count: {state.count}</p>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
/>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}Custom Hooks: Reusable Logic
Custom Hooks let you extract component logic into reusable functions. A custom Hook is a function whose name starts with use and that may call other Hooks.
// useLocalStorage — persist state to localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
// useDebounce — debounce a rapidly changing value
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// useFetch — generic data fetching
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false); } })
.catch(err => { if (!cancelled) { setError(err); setLoading(false); } });
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', '');
const debouncedName = useDebounce(name, 300);
const { data, loading } = useFetch<User[]>(`/api/search?q=${debouncedName}`);
}Rules of Hooks
There are two essential rules you must follow when using Hooks.
- Only call Hooks at the top level. Do not call Hooks inside loops, conditions, or nested functions. This ensures Hooks are called in the same order each render.
- Only call Hooks from React functions. Call Hooks from React function components or custom Hooks, not from regular JavaScript functions.
// WRONG — Hook inside a condition
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // breaks Hook order
}
}
// CORRECT — condition inside the Hook
function Good({ isLoggedIn }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (isLoggedIn) fetchUser().then(setUser);
}, [isLoggedIn]);
}Common Pitfalls and Solutions
Stale Closures in useEffect
When you reference state inside useEffect without including it in the dependency array, you get a stale closure — the effect sees an old value.
// BUG: stale closure
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // always logs 0 (stale!)
setCount(count + 1); // always sets to 1
}, 1000);
return () => clearInterval(id);
}, []); // count is missing from dependencies
}
// FIX: use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // always uses latest value
}, 1000);
return () => clearInterval(id);
}, []); // safe with functional updateInfinite Loops with useEffect
Setting state inside useEffect without proper dependencies causes infinite re-render loops.
// BUG: infinite loop
useEffect(() => {
setCount(count + 1); // triggers re-render, which runs effect again
}); // no dependency array = runs every render
// FIX: add dependency array
useEffect(() => {
if (count < 10) setCount(count + 1);
}, [count]); // only runs when count changesObject/Array Dependencies
Objects and arrays are compared by reference, not value. Creating them inline causes effects to run every render.
// BUG: new object every render
function App() {
const options = { page: 1, limit: 10 }; // new ref each render
useEffect(() => {
fetchData(options);
}, [options]); // runs every render!
}
// FIX: useMemo to stabilize the reference
function App() {
const options = useMemo(() => ({ page: 1, limit: 10 }), []);
useEffect(() => {
fetchData(options);
}, [options]); // stable reference
}Frequently Asked Questions
What are React Hooks and why should I use them?
React Hooks are functions that let you use React features like state, lifecycle, and context in functional components. They make code more readable, reusable, and testable compared to class components. Hooks also enable better code splitting and composition through custom Hooks.
What is the difference between useMemo and useCallback?
useMemo memoizes a computed value, while useCallback memoizes a function reference. useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useMemo for expensive calculations and useCallback for stable function references passed to child components.
When should I use useReducer instead of useState?
Use useReducer when state logic is complex, involves multiple sub-values, or when the next state depends on the previous one. It is also useful when you want to pass dispatch down instead of callbacks, making the component logic more predictable.
Can I use Hooks in class components?
No, Hooks only work in functional components and custom Hooks. However, you can mix class and function components in the same application. You can wrap a Hook in a function component and use it alongside class components.
How do I avoid infinite loops with useEffect?
Always specify the correct dependency array. An empty array [] runs the effect once on mount. Include all variables the effect reads. For objects and arrays, use useMemo to stabilize references, or extract the specific primitive values you need.
React Hooks are essential for modern React development. Master these core Hooks and you will be able to build any UI efficiently. Start with useState and useEffect, then gradually adopt useMemo, useCallback, and custom Hooks as your applications grow.