Hooks Deep Dive
useContext, useReducer, useMemo, useCallback, useRef, custom hooks
React Hooks are functions that let you use state and other React features in function components. Beyond useState and useEffect, the advanced hooks — useContext, useReducer, useMemo, useCallback, useRef — enable complex state logic, performance optimisation, and DOM interaction. Custom hooks are the primary reuse mechanism in React.
Key Points
- useContext: reads a Context value without prop drilling — combine with useState in a provider for app-wide state
- useReducer: useState for complex state logic — dispatch(action) → reducer(state, action) → newState; like Redux at component level
- useMemo(fn, deps): memoises the result of a computation — re-runs only when deps change; for expensive calculations
- useCallback(fn, deps): memoises a function reference — prevents child re-renders caused by new function instances on each render
- useRef: mutable ref object whose .current property persists across renders without causing re-renders — DOM refs, interval IDs, prev values
- Custom hooks: function starting with use that calls other hooks — extract and share stateful logic between components
- Rules of hooks: only call at the top level (no conditionals/loops), only in React function components or custom hooks
- useId (React 18): generate stable, unique ID for accessibility labels — server-client consistent
- useDeferredValue / useTransition (React 18): mark updates as non-urgent — keep UI responsive during expensive renders
React hooks: custom useFetch with useReducer, Context + useReducer global state, useCallback, useRef for intervals
import { useContext, useReducer, useMemo, useCallback, useRef, createContext } from 'react';
// Custom hook — fetch with loading/error state
function useFetch<T>(url: string) {
const [state, dispatch] = useReducer(
(s: State<T>, a: Action<T>) => {
switch (a.type) {
case 'loading': return { ...s, loading: true, error: null };
case 'success': return { data: a.data, loading: false, error: null };
case 'error': return { ...s, loading: false, error: a.error };
default: return s;
}
},
{ data: null, loading: true, error: null }
);
useEffect(() => {
const ctrl = new AbortController();
dispatch({ type: 'loading' });
fetch(url, { signal: ctrl.signal })
.then(r => r.json())
.then(data => dispatch({ type: 'success', data }))
.catch(e => { if (e.name !== 'AbortError') dispatch({ type: 'error', error: e.message }); });
return () => ctrl.abort();
}, [url]);
return state;
}
// Context + useReducer — global state without Redux
const CartContext = createContext<CartContextValue | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
const addItem = useCallback((item: CartItem) => dispatch({ type: 'ADD', item }), []);
const removeItem = useCallback((id: string) => dispatch({ type: 'REMOVE', id }), []);
const total = useMemo(() => cart.items.reduce((sum, i) => sum + i.price * i.qty, 0), [cart.items]);
return (
<CartContext.Provider value={{ cart, addItem, removeItem, total }}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
};
// useRef — DOM access + interval tracking
function Timer() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef<number | null>(null); // won't trigger re-render
function start() {
intervalRef.current = window.setInterval(() => setSeconds(s => s + 1), 1000);
}
function stop() {
if (intervalRef.current) clearInterval(intervalRef.current);
}
useEffect(() => () => stop(), []); // cleanup on unmount
return <div>{seconds}s <button onClick={start}>Start</button></div>;
}Real-World Example
useMemo and useCallback are often over-used. Only memoize when (1) a calculation is measurably expensive (measured with React DevTools profiler) or (2) a function/value is a dependency of another hook or passed to a memo-wrapped child. Wrapping everything in useMemo adds complexity without benefit — React's rendering is already very fast for most component sizes.