Quick Answer

A custom hook is a function whose name begins with use and which calls other hooks. It lets you extract stateful logic from a component so several components can reuse it. Each component calling the hook gets its own independent state — the hook shares the logic, not the data. The use prefix is not decoration; it is how the linter knows to enforce the rules of hooks.

Extracting Logic, Not Markup

Components often repeat the same stateful logic. Two components both fetching data need the same loading flag, error state and effect.

// Repeated in every component that fetches
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => { /* fetch, set all three */ }, [url]);

Extract it into a function that calls hooks:

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    fetch(url, { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);   // fetch does not throw on 404
        return r.json();
      })
      .then(setData)
      .catch(e => { if (e.name !== 'AbortError') setError(e); })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

Now any component is three lines:

const { data, loading, error } = useFetch('/api/users');

Note what was extracted: logic and state, not JSX. That is the distinction between a custom hook and a component.

It Shares Logic, Not State

This is the point that most often needs clarifying, and it is a common interview question.

function A() { const { data } = useFetch('/api/posts'); }
function B() { const { data } = useFetch('/api/users'); }

These do not share anything at runtime. Each call creates its own useState and its own useEffect. Two components calling the same hook have completely independent state.

The hook is a recipe, not a store. Calling it runs the recipe again with fresh ingredients.

That means a custom hook is not the way to share state between components. For that you need lifted state, Context, or a state library. Reaching for a custom hook to make two components stay in sync is a common mistake, and the symptom is two components that each hold their own copy and drift apart.

It also means custom hooks compose naturally without interference:

function useUserDashboard(userId) {
  const user = useFetch(`/api/users/${userId}`);
  const posts = useFetch(`/api/users/${userId}/posts`);
  const [tab, setTab] = useLocalStorage('dashboard-tab', 'profile');
  return { user, posts, tab, setTab };
}

Each inner hook keeps its own state, and the outer hook simply returns a combined shape.

The Rules, and Why the Name Matters

The name must start with use. This is not a style convention — it is how tooling identifies a hook. The ESLint rules-of-hooks plugin uses the prefix to know it should enforce the hook rules inside that function. Name it fetchData instead of useFetch and you lose every warning about conditional or nested hook calls.

Call hooks only at the top level. Not inside conditions, loops or nested functions. React tracks hooks by call order, so a conditional hook shifts every subsequent one and state attaches to the wrong variable.

function useThing(enabled) {
  if (enabled) {
    const [x, setX] = useState(0);   // WRONG — order changes between renders
  }
}

function useThing(enabled) {
  const [x, setX] = useState(0);     // always called
  return enabled ? x : null;         // condition on the result instead
}

Call hooks only from components or other hooks. Not from plain functions, event handlers or class components.

Return whatever shape suits. An array when the names are up to the caller, like useState; an object when there are several named values, which is clearer for three or more.

return [value, setValue];              // caller names them
return { data, loading, error };       // self-documenting

Four Hooks Worth Having

useLocalStorage — state that survives a refresh.

function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initial;
    } catch {
      return initial;          // storage can be unavailable or the JSON invalid
    }
  });

  useEffect(() => {
    try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
  }, [key, value]);

  return [value, setValue];
}

Note the lazy initial state — reading localStorage on every render would be wasteful, and the function form runs it once.

useDebounce — delay a fast-changing value, for search inputs.

function useDebounce(value, delay = 500) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);    // cancel if value changes first
  }, [value, delay]);
  return debounced;
}

useToggle — small, but removes repetitive boilerplate.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = useCallback(() => setOn(v => !v), []);
  return [on, toggle];
}

useMediaQuery — respond to viewport changes in JavaScript.

function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
  useEffect(() => {
    const mql = window.matchMedia(query);
    const handler = e => setMatches(e.matches);
    mql.addEventListener('change', handler);
    return () => mql.removeEventListener('change', handler);
  }, [query]);
  return matches;
}

When to Extract, and When Not To

Extract when: the same stateful logic appears in two or three places; a component's logic has grown large enough to obscure its markup; or the logic is independently testable and worth naming.

Do not extract when: it is used once and is unlikely to be reused. A hook used in a single component adds indirection without benefit — you now read two files to understand one component.

Wait for the second or third use. Premature extraction usually produces a hook with too many parameters, because it was generalised before you knew what varied.

Two mistakes worth avoiding.

Hooks that do too much. A useEverything returning fifteen values is a component in disguise. Prefer several small hooks and compose them.

Forgetting cleanup. Any hook that subscribes, sets an interval or starts a request must return a cleanup function. Without it you get memory leaks and state updates on unmounted components — and because the hook is reused, the bug appears everywhere at once.

Finally, custom hooks are genuinely testable in isolation, which is a real advantage over logic embedded in components. Testing library helpers exist specifically for rendering a hook without a component around it.

Frequently Asked Questions

Do custom hooks share state between components? No. Each component calling a hook gets its own independent state, because the hook simply runs its useState and useEffect calls again. Hooks share logic, not data — for shared state use lifted state, Context or a state library.
Why must a custom hook start with use? Because tooling identifies hooks by that prefix. The ESLint rules-of-hooks plugin uses it to enforce that hooks are not called conditionally or in loops, so renaming the function silently disables those warnings.
Can a custom hook call other custom hooks? Yes, and composing them is the normal pattern. Each inner hook keeps its own state, and the outer hook returns whatever combined shape is convenient for callers.
When should I create a custom hook? When the same stateful logic appears in two or three components, or when a component's logic has grown large enough to obscure its markup. Extracting after a single use usually adds indirection without benefit.
Should a custom hook return an array or an object? An array when the caller should name the values themselves, as useState does, which suits two values. An object when there are three or more, since named properties are self-documenting and order-independent.