React Custom Hooks

Extract and share stateful component logic into reusable, composable custom hook functions.

TL;DR

  1. Extract repeated stateful logic into custom functions prefixed with use.
  2. Compose built-in primitives like useState and useEffect inside custom hooks.
  3. Return tuples or structured objects to provide flexible consumer interfaces.

Hook Extraction Basics

    Boolean Toggle Hook

    Encapsulate boolean state toggling into a reusable helper.

    export function useToggle(initial = false) {
      const [val, setVal] = useState(initial);
      const toggle = useCallback(() => {
        setVal((v) => !v);
      }, []);
      return [val, toggle] as const;
    }
    Consuming Custom Hook

    Call custom hook inside any functional React component.

    export function ModalTrigger() {
      const [isOpen, toggleOpen] = useToggle();
      return <Button onClick={toggleOpen}>Toggle</Button>;
    }
    Window Dimension Hook

    Track window resize events synchronized with state.

    export function useWindowSize() {
      const read = () => ({
        w: window.innerWidth,
        h: window.innerHeight,
      });
      const [size, setSize] = useState(read);
      useEffect(() => {
        const onResize = () => setSize(read());
        window.addEventListener('resize', onResize);
        return () =>
          window.removeEventListener('resize', onResize);
      }, []);
      return size;
    }

Persistence and Storage Hooks

    LocalStorage Hook

    Persist state in localStorage.

    export function useLocalStorage<T>(
      key: string, init: T
    ) {
      const [value, setValue] = useState<T>(() => {
        const saved = localStorage.getItem(key);
        return saved ? JSON.parse(saved) : init;
      });
      useEffect(() => {
        localStorage.setItem(key, JSON.stringify(value));
      }, [key, value]);
      return [value, setValue] as const;
    }
    Document Title Hook

    Sync the tab title with state.

    export function useDocumentTitle(title: string) {
      useEffect(() => {
        document.title = title;
      }, [title]);
    }
    Media Query Hook

    Track a CSS media query.

    export function useMediaQuery(query: string) {
      const [match, setMatch] = useState(false);
      useEffect(() => {
        const media = matchMedia(query);
        media.onchange = () => setMatch(media.matches);
        setMatch(media.matches);
        return () => { media.onchange = null; };
      }, [query]);
      return match;
    }

Async and Data Hooks

    Async Fetch Hook

    Fetch JSON and track loading.

    export function useFetch<T>(url: string) {
      const [data, setData] = useState<T | null>(null);
      const [loading, setLoading] = useState(true);
      useEffect(() => {
        fetch(url).then(r => r.json()).then(setData)
          .finally(() => setLoading(false));
      }, [url]);
      return { data, loading };
    }
    Debounced Value Hook

    Wait for input to pause.

    export function useDebounce<T>(value: T, delay = 300) {
      const [result, setResult] = useState(value);
      useEffect(() => {
        const timer = setTimeout(() => setResult(value), delay);
        return () => clearTimeout(timer);
      }, [value, delay]);
      return result;
    }
    Previous Value Hook

    Keep the prior render's value.

    export function usePrevious<T>(value: T) {
      const ref = useRef<T>();
      useEffect(() => { ref.current = value; }, [value]);
      return ref.current;
    }

Hook Design Best Practices

    Single Responsibility

    Build focused micro-hooks that compose gracefully together.

    // Good: Focused hooks
    const isMobile = useMediaQuery('(max-width: 768px)');
    const theme = useLocalStorage('theme', 'dark');
    Return Tuple as const

    Mark array returns with as const for exact tuple types.

    return [state, actions] as const;
    Lint Enforced Rules

    Ensure eslint-plugin-react-hooks is active in project.

    // .eslintrc.json
    {
      "extends": ["plugin:react-hooks/recommended"]
    }

Tips

  1. Prefix all custom hook function names with use so React ESLint plugins can enforce Rules of Hooks automatically.
  2. Keep custom hooks focused on a single responsibility to maximize reusability across unrelated visual components.

Warnings

  1. Custom hooks share stateful logic, not state itself; each component calling a custom hook gets isolated independent state.
  2. Never invoke hooks inside conditional blocks, nested loops, or helper functions inside your custom hook body.

In Practice

FAQ