React Hooks
Quick reference for core and advanced React hooks with usage rules and patterns.
TL;DR
- 01useState and useEffect cover most common state and side-effect needs.
- 02Use useCallback and useMemo only when you have a proven performance issue.
- 03Only call hooks at the top level and inside React function components.
Tips
- 01Create custom hooks to share logic between components — extract state and effects into reusable hooks with names starting with "use".
Warnings
- 01Always follow the Rules of Hooks — calling hooks conditionally or from non-component functions breaks React's internal state management.
Core Hooks
- useState for managing local component state.
const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; - useEffect for side effects and cleanup.
useEffect(() => { fetch("/api/data").then(r => r.json()).then(setData); return () => { /* cleanup */ }; }, []); - useContext for accessing shared data from a provider.
const { theme } = useContext(ThemeContext); return <div style={{ background: theme }}>Content</div>; - useReducer for state with multiple related actions.
const [state, dispatch] = useReducer(reducer, { count: 0 }); return <button onClick={() => dispatch({ type: "INCREMENT" })}> {state.count} </button>; - useRef for accessing DOM nodes or storing mutable values.
const inputRef = useRef(null); return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> </> );
Performance Hooks
- useCallback memoizes a function reference across renders.
const handleClick = useCallback(() => { doSomething(value); }, [value]); - useMemo memoizes a computed value across renders.
const expensiveValue = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]); - Use useCallback to prevent child re-renders when passing callbacks.
const handleSubmit = useCallback((data) => { saveData(data); }, []); // stable reference — MemoizedChild won't re-render return <MemoizedChild onSubmit={handleSubmit} />; - Use useMemo to avoid recalculating expensive derived data.
const sortedItems = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items] ); - Both hooks only optimize — they don't change behavior.
// Add these only after profiling shows a real bottleneck // Premature optimization adds complexity without benefit
Ref Hooks
- useRef creates a mutable ref that persists across renders.
const inputRef = useRef(null); return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> </> ); - useImperativeHandle exposes custom methods to parent components.
useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ""; } })); - Store mutable values that don't need to trigger re-renders.
const timerRef = useRef(null); function start() { timerRef.current = setInterval(tick, 1000); } function stop() { clearInterval(timerRef.current); } - Track previous prop or state value for comparisons.
const prevCountRef = useRef(count); useEffect(() => { prevCountRef.current = count; }, [count]); const prevCount = prevCountRef.current; // value from last render - Forward refs to child components with forwardRef.
const Input = forwardRef((props, ref) => ( <input ref={ref} {...props} /> ));
Advanced Hooks
- useLayoutEffect runs synchronously before paint — use for measurements.
useLayoutEffect(() => { // Runs before browser paints — safe to read DOM measurements here const height = elementRef.current.offsetHeight; setHeight(height); }, []); - useDebugValue shows hook values in React DevTools.
function useCustomHook(value) { useDebugValue(value > 10 ? "large" : "small"); return value; } - useTransition marks an update as non-urgent to keep UI responsive.
const [isPending, startTransition] = useTransition(); startTransition(() => { setSearchResults(results); // won't block input from being typed }); - useDeferredValue defers a value update without a transition.
const deferredQuery = useDeferredValue(searchQuery); const results = useMemo(() => filterList(deferredQuery), [deferredQuery]); - useId generates a stable unique ID for accessibility attributes.
function Input({ label }) { const id = useId(); return ( <> <label htmlFor={id}>{label}</label> <input id={id} /> </> ); }
Hook Rules
- Only call hooks at the top level, never in loops or conditions.
// Good function Component() { const [count, setCount] = useState(0); useEffect(() => { /* ... */ }, []); } // Bad: hook inside condition if (condition) { const [count, setCount] = useState(0); // Wrong! } - Only call hooks from React components or custom hooks.
- Use ESLint plugin to enforce rules automatically.
{ "plugins": ["react-hooks"], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } } - Custom hooks must start with "use" so React enforces the rules.
// Good: React treats useWindowWidth as a hook function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handler = () => setWidth(window.innerWidth); window.addEventListener("resize", handler); return () => window.removeEventListener("resize", handler); }, []); return width; } - Hooks must be called in the same order every render.
// React tracks hooks by call order — conditional calls break this // Move conditions inside the hook body, not around it useEffect(() => { if (!isLoggedIn) return; // condition inside — safe }, [isLoggedIn]);
FAQ
Pass a dependency array as the second argument to useEffect — an empty array [] runs the effect once on mount, while listing specific values like [userId] re-runs only when those values change. Avoid placing the fetch URL or options object directly in the dependency array if they're recreated each render, as object identity changes will trigger the effect repeatedly.
useState triggers a re-render when updated, while useRef stores a mutable value in .current that persists across renders without causing re-renders. Use useRef for values you need to track (like timers, previous values, or DOM nodes) but don't want to drive the UI.
Use them only after profiling confirms a real performance problem — wrapping every function or computed value adds overhead and complexity that often outweighs any benefit. The most justified cases are passing stable callbacks to heavily optimized child components (React.memo) or skipping expensive recalculations in tight render loops.
React 18 Strict Mode intentionally double-invokes effects in development to surface bugs in effects that don't properly clean up. Return a cleanup function from your effect to handle unmounting correctly, and your production build will only run the effect once.
Extract the state and effects into a custom hook — a plain JavaScript function whose name starts with 'use' that calls other hooks internally. Each component that calls the custom hook gets its own isolated state, so sharing logic doesn't mean sharing state.