SyntaxStudy
Sign Up
React Advanced Custom Hooks: usePrevious and useOnClickOutside
React Beginner 1 min read

Advanced Custom Hooks: usePrevious and useOnClickOutside

usePrevious is a hook that captures the value a prop or state variable held during the previous render. It works by storing the value in a ref inside a useEffect, which runs after the render and therefore sees the current value while the ref still holds the previous one. This pattern is useful for animating transitions, comparing before and after values, or implementing undo functionality. useOnClickOutside attaches a mousedown event listener to the document and fires a callback when the click target is outside a given DOM node. It is the building block for closing modals, dropdowns, and popovers when the user clicks away. The hook accepts a ref pointing to the container element and a handler function, encapsulating the addEventListener and removeEventListener lifecycle entirely. These examples show how custom hooks can abstract surprisingly nuanced patterns — timing of effects, stable callback references, DOM event delegation — into a one-liner at the call site.
Example
import { useEffect, useRef } from 'react';

// ── usePrevious ────────────────────────────────────────────────────────────
function usePrevious(value) {
    const ref = useRef(undefined);
    // Runs AFTER the render, so ref still holds previous value during render
    useEffect(() => {
        ref.current = value;
    });
    return ref.current;
}

// ── useOnClickOutside ──────────────────────────────────────────────────────
function useOnClickOutside(ref, handler) {
    useEffect(() => {
        function listener(event) {
            if (!ref.current || ref.current.contains(event.target)) return;
            handler(event);
        }
        document.addEventListener('mousedown', listener);
        document.addEventListener('touchstart', listener);
        return () => {
            document.removeEventListener('mousedown', listener);
            document.removeEventListener('touchstart', listener);
        };
    }, [ref, handler]);
}

// ── Usage ──────────────────────────────────────────────────────────────────
function Dropdown({ items }) {
    const [open, setOpen] = React.useState(false);
    const containerRef    = useRef(null);

    useOnClickOutside(containerRef, () => setOpen(false));

    return (
        <div ref={containerRef}>
            <button onClick={() => setOpen(o => !o)}>Menu</button>
            {open && (
                <ul>
                    {items.map(item => <li key={item}>{item}</li>)}
                </ul>
            )}
        </div>
    );
}

function CounterWithPrev() {
    const [count, setCount] = React.useState(0);
    const prev = usePrevious(count);
    return (
        <p>
            Now: {count}, Before: {prev ?? 'n/a'}
            <button onClick={() => setCount(c => c + 1)}>+</button>
        </p>
    );
}