Every essential React Hook explained with real-world examples — useState, useEffect, useContext, useRef, useMemo, useCallback, useReducer, custom hooks and more. For beginners to advanced React developers.
Manage local component state. Returns a stateful value and a setter function — calling the setter triggers a re-render.
import { useState } from 'react'; function Counter() { // [value, setter] = useState(initialValue) const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> <button onClick={() => setCount(0)}> Reset </button> </div> ); }
// ✅ Functional update (safe for async) setCount(prev => prev + 1); // Object state — spread to keep props const [user, setUser] = useState({ name: 'Alice', age: 30 }); setUser(prev => ({ ...prev, // keep existing fields age: 31 // override one field })); // Array state const [items, setItems] = useState([]); // Add item setItems(prev => [...prev, newItem]); // Remove item setItems(prev => prev.filter(i => i.id !== id) );
// ❌ Runs every render (expensive) const [data, setData] = useState( computeExpensiveValue() ); // ✅ Lazy init — runs ONCE const [data, setData] = useState( () => computeExpensiveValue() ); // Real-world: read from localStorage const [theme, setTheme] = useState( () => localStorage.getItem('theme') ?? 'dark' ); // Boolean toggle pattern const [open, setOpen] = useState(false); const toggle = () => setOpen(prev => !prev);
Synchronise a component with an external system. Runs after every render by default; control it with the dependency array.
useEffect(() => { /* runs after EVERY render */ }); useEffect(() => { /* runs ONCE on mount */ }, []); useEffect(() => { /* runs when userId changes */ fetchUser(userId); }, [userId]); useEffect(() => { /* multiple deps */ fetchData(page, filter); }, [page, filter]);
useEffect(() => { // Subscribe const sub = store.subscribe(handler); // Cleanup on unmount / re-run return () => sub.unsubscribe(); }, []); // Timer cleanup useEffect(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); }, []); // Event listener cleanup useEffect(() => { window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);
useEffect(() => { const ctrl = new AbortController(); async function load() { try { setLoading(true); const res = await fetch(url, { signal: ctrl.signal }); const data = await res.json(); setData(data); } catch (e) { if (e.name !== 'AbortError') setError(e); } finally { setLoading(false); } } load(); return () => ctrl.abort(); }, [url]);
Consume a React context without nesting. Pair with createContext and a provider to share state across the component tree.
// 1. Create context with default value const ThemeContext = createContext('light'); // 2. Provide value at top of tree function App() { const [theme, setTheme] = useState('dark'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Layout /> </ThemeContext.Provider> ); }
// 3. Consume — any depth, no drilling function Button() { const { theme, setTheme } = useContext(ThemeContext); return ( <button className={theme} onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark') }> Toggle </button> ); }
// auth-context.jsx const AuthContext = createContext(null); export function AuthProvider({ children }) { const [user, setUser] = useState(null); return ( <AuthContext.Provider value={{ user, setUser }}> {children} </AuthContext.Provider> ); } // Custom hook with guard export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth outside Provider'); return ctx; }
Hold a mutable value that persists across renders without causing a re-render. Perfect for DOM references and previous-value tracking.
function SearchBox() { const inputRef = useRef(null); useEffect(() => { // Focus input on mount inputRef.current?.focus(); }, []); const handleClear = () => { inputRef.current.value = ''; inputRef.current.focus(); }; return ( <> <input ref={inputRef} type="text" /> <button onClick={handleClear}> Clear </button> </> ); }
// Store timer ID without re-render const timerId = useRef(null); const start = () => { timerId.current = setInterval(tick, 1000); }; const stop = () => { clearInterval(timerId.current); }; // Track previous value function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; // previous }
// Child exposes custom methods const FancyInput = forwardRef( (props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ''; } })); return <input ref={inputRef} />; } ); // Parent usage const ref = useRef(); <FancyInput ref={ref} /> ref.current.focus();
Memoize expensive computed values — only recomputes when dependencies change. Skip unnecessary re-computations.
const sortedList = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name) ), [items] // only re-sort when items changes ); // Filter + map pipeline const results = useMemo(() => products .filter(p => p.name.toLowerCase() .includes(query.toLowerCase())) .map(p => ({ ...p, label: `${p.name} — $${p.price}` })), [products, query] );
| ✅ Use useMemo | ❌ Skip it |
|---|---|
| Sort / filter large arrays | Simple arithmetic |
| Heavy math / data transform | String concatenation |
| Referential equality for children | Primitive state values |
| Context value objects | Functions (use useCallback) |
| D3 / canvas computations | Small arrays (< 100 items) |
// ❌ New object every render const config = { color: theme, size: 16 }; // Child re-renders even if theme unchanged <Child config={config} /> // ✅ Stable reference const config = useMemo( () => ({ color: theme, size: 16 }), [theme] ); // Context value — avoid re-renders const ctxValue = useMemo( () => ({ user, login, logout }), [user] ); <Ctx.Provider value={ctxValue}> … </>
Memoize function references — returns the same function unless dependencies change. Essential when passing callbacks to memoized children.
// ❌ New fn every render → child re-renders const handleClick = () => doSomething(id); // ✅ Stable reference const handleClick = useCallback( () => doSomething(id), [id] ); // With React.memo child const MemoChild = memo(function Child({ onClick }) { return <button onClick={onClick}>Click</button>; }); // Parent const handler = useCallback(() => {}, []); <MemoChild onClick={handler} />
| Hook | Returns | Use For |
|---|---|---|
useMemo |
Value | Computed results |
useCallback |
Function | Event handlers, callbacks |
// These are equivalent: useCallback(fn, [deps]); useMemo(() => fn, [deps]); // useCallback for event handlers const onSubmit = useCallback(async (e) => { e.preventDefault(); await saveData(formData); }, [formData]);
// ❌ Infinite loop — fetch recreated every render const fetchData = () => api(id); useEffect(() => { fetchData(); }, [fetchData]); // ✅ Stable with useCallback const fetchData = useCallback( () => api(id), [id] ); useEffect(() => { fetchData(); }, [fetchData]); // only re-runs when id changes
Manage complex state logic with a reducer function. Great for state machines, multi-action updates, and when next state depends on action type.
const initialState = { count: 0, step: 1 }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + state.step }; case 'SET_STEP': return { ...state, step: action.payload }; case 'RESET': return initialState; default: return state; } } const [state, dispatch] = useReducer(reducer, initialState); dispatch({ type: 'INCREMENT' }); dispatch({ type: 'SET_STEP', payload: 5 });
const init = { values: { name: '', email: '' }, loading: false, error: null }; function formReducer( state, action ) { switch (action.type) { case 'FIELD': return { ...state, values: { ...state.values, [action.field]: action.value } }; case 'SUBMIT': return { ...state, loading: true, error: null }; case 'ERROR': return { ...state, loading: false, error: action.message }; default: return state; } }
| useState | useReducer |
|---|---|
| Simple values | Complex objects |
| Independent state | Related state fields |
| Few transitions | Many action types |
| Local only | Shareable reducer logic |
| No history needed | Undo / redo patterns |
// Lazy init with 3rd arg const [s, dispatch] = useReducer( reducer, userId, // arg createInitialState // init fn );
Extract and reuse stateful logic across components. Any function starting with "use" that calls other hooks is a custom hook.
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const ctrl = new AbortController(); fetch(url, { signal: ctrl.signal }) .then(r => r.json()) .then(d => { setData(d); setLoading(false); }) .catch(e => { if (e.name !== 'AbortError') { setError(e); setLoading(false); } }); return () => ctrl.abort(); }, [url]); return { data, loading, error }; } // Usage const { data, loading } = useFetch('/api/users');
function useLocalStorage(key, initial) { const [value, setValue] = useState(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initial; } catch { return initial; } }); const set = useCallback(v => { setValue(v); localStorage.setItem( key, JSON.stringify(v) ); }, [key]); return [value, set]; } // Usage const [theme, setTheme] = useLocalStorage('theme', 'dark');
function useDebounce(value, delay) { const [debounced, set] = useState(value); useEffect(() => { const t = setTimeout(() => set(value), delay); return () => clearTimeout(t); }, [value, delay]); return debounced; } function useWindowSize() { const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight }); useEffect(() => { const onResize = () => setSize({ w: window.innerWidth, h: window.innerHeight }); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); return size; }
useLayoutEffect, useId, useDeferredValue, useTransition, useSyncExternalStore and more React 18+ hooks.
// useEffect: async, after paint // useLayoutEffect: sync, before paint const ref = useRef(null); const [height, setHeight] = useState(0); useLayoutEffect(() => { // Read DOM before browser paints setHeight(ref.current.offsetHeight); }, []); // ✅ Good: prevent visual flicker // ❌ Avoid on SSR (use useEffect) // ❌ Don't fetch data here
// useTransition — mark slow updates const [ isPending, startTransition ] = useTransition(); const handleSearch = ( e ) => { setInput( e.target.value ); // urgent startTransition(() => { setResults( filter( e.target.value ) ); }); }; {isPending && <Spinner />} // useDeferredValue — defer a value const deferred = useDeferredValue( query ); // deferred lags behind query — // slow list renders with old value
// useId — stable unique IDs (SSR-safe) function FormField({ label }) { const id = useId(); return ( <> <label htmlFor={id}> {label} </label> <input id={id} /> </> ); } // useSyncExternalStore — subscribe function useOnlineStatus() { return useSyncExternalStore( navigator.onLine ? (cb) => { window.addEventListener( 'offline', cb ); return () => window.removeEventListener( 'offline', cb ); } : (cb) => { window.addEventListener( 'online', cb ); return () => window.removeEventListener( 'online', cb ); }, () => navigator.onLine ); }
Two mandatory rules enforced by the ESLint plugin — break them and you get subtle, hard-to-debug bugs.
// ❌ RULE 1 VIOLATION — conditional hook if (condition) { const [x, setX] = useState(0); // 🚫 } // ✅ Move condition INSIDE the hook const [x, setX] = useState( condition ? 0 : null ); // ❌ RULE 2 VIOLATION — hook in callback function handleClick() { const [v, set] = useState(); // 🚫 } // ❌ RULE 2 — hook in loop items.forEach(item => { useEffect(() => {}); // 🚫 }); // ✅ Hooks at top level of component only
| Situation | Hook to Use |
|---|---|
| Store a UI value that triggers re-render | useState |
| Complex state with multiple actions | useReducer |
| Fetch data / subscribe to events | useEffect |
| Share state across component tree | useContext |
| Reference a DOM element | useRef |
| Store value without re-render | useRef |
| Cache expensive computation | useMemo |
| Stable function reference | useCallback |
| Measure DOM before paint | useLayoutEffect |
| Keep UI responsive on slow updates | useTransition |
| Unique IDs for accessibility | useId |
Keep levelling up with our other free developer references — hand-crafted by K2Infocom.