Custom Hooks

React · Practical · 12 min

Custom Hooks are React's main tool for reusing behaviour and keeping components readable.

When several components need the same stateful logic (subscribing to online status, reading window size, a debounced value), extract it into a custom Hook: a function whose name starts with use and that calls other Hooks.

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  useEffect(() => { /* subscribe to online/offline events */ }, []);
  return isOnline;
}

Any component can now write const isOnline = useOnlineStatus();.

Important properties:

  • Custom Hooks share logic, not state. Each call gets its own independent state, exactly as if the component had called useState and useEffect itself.
  • They follow the Rules of Hooks: call them at the top level, and only from components or other Hooks. The use prefix is what lets the linter (and the React Compiler) know the rules apply.
  • They receive fresh arguments on every render, so they can react to changing props: useChatRoom({ roomId }).
  • A function that doesn't call any Hooks shouldn't be named useSomething; make it a plain function.

Good custom Hooks are named after what they do for the component (useOnlineStatus, useFormField, useMediaQuery), hiding the effect underneath. Generic "lifecycle" wrappers like useMount tend to hide bad effect usage rather than help.

Custom Hooks are also a nice migration seam: if you later replace your hand-written effect with useSyncExternalStore or a data library, the components calling the Hook don't change.

Example

import { useEffect, useState } from "react";

// Debounce any changing value
function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);
  return debounced;
}

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initial;
  });
  useEffect(() => localStorage.setItem(key, JSON.stringify(value)), [key, value]);
  return [value, setValue];
}

export default function SearchBox() {
  const [query, setQuery] = useLocalStorage("query", "");
  const debounced = useDebouncedValue(query, 500);
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <p>Searching for: {debounced}</p>
    </>
  );
}

Remember

  • A custom Hook is a use-prefixed function that calls other Hooks
  • It shares logic, not state: every call has its own state
  • Name Hooks by purpose (useOnlineStatus), not by lifecycle (useMount)

Watch out for

  • Prefixing a plain helper with use even though it calls no Hooks, which needlessly restricts where it can be called.

Check yourself

  1. Two components both call useCounter(). One increments its count. What happens to the other's count?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/reusing-logic-with-custom-hooks