AniUI Academy

Custom Hooks

Extracting reusable stateful logic into your own function starting with "use," the rules of hooks, and why two components sharing a custom hook still get independent state.

9 min read

useState, useEffect, and useRef are the building blocks; a custom hook is what you get when you notice the same combination of them, solving the same problem, showing up in more than one component — and pull it out into its own reusable function.

A motivating duplication

Imagine two components that both need to know if the browser window is currently wide or narrow:

function Sidebar() {
  const [isWide, setIsWide] = useState(window.innerWidth > 768);
 
  useEffect(() => {
    function handleResize() {
      setIsWide(window.innerWidth > 768);
    }
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);
 
  return isWide ? <FullSidebar /> : <CollapsedSidebar />;
}
 
function Header() {
  const [isWide, setIsWide] = useState(window.innerWidth > 768);
  // ...the exact same effect, duplicated
}

The same subscribe/cleanup logic, copied. If the breakpoint ever needs to change, or the resize handling needs debouncing, there are now two places to remember to update — and they will eventually drift.

Extracting the hook

A custom hook is nothing more than this same logic, moved into its own function whose name starts with use:

function useIsWide(breakpoint = 768) {
  const [isWide, setIsWide] = useState(window.innerWidth > breakpoint);
 
  useEffect(() => {
    function handleResize() {
      setIsWide(window.innerWidth > breakpoint);
    }
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, [breakpoint]);
 
  return isWide;
}
 
function Sidebar() {
  const isWide = useIsWide();
  return isWide ? <FullSidebar /> : <CollapsedSidebar />;
}
 
function Header() {
  const isWide = useIsWide(1024); // a different breakpoint, still one definition
}

Both components now share one definition of "how do we know the window is wide" — fix a bug once, and every caller benefits.

The use prefix isn't decoration

Naming it useIsWide (not getIsWide or isWide) matters because it's what tells React's linter, and other developers reading the code, that this function calls hooks internally and therefore has to follow the same rules real hooks follow.

The rules of hooks

Two rules, both there for the same underlying reason:

  1. Only call hooks at the top level — never inside a condition, loop, or nested function.
  2. Only call hooks from React function components or other custom hooks — not from regular functions or event handlers directly.

The reason is mechanical: React doesn't track hook state by name, it tracks it by call order, on every render. The first useState call always maps to the same stored slot, the second always maps to the next slot, and so on — regardless of what the hooks are named. If a hook call were skipped on some renders (because it was inside an if), every hook call after it would shift by one slot and silently attach to the wrong stored state.

// Breaks the rule — conditional hook call
function Broken({ shouldTrack }) {
  if (shouldTrack) {
    const [count, setCount] = useState(0); // sometimes called, sometimes not
  }
  const [name, setName] = useState(""); // this hook's "slot" shifts depending on shouldTrack
}

Hooks share logic, not state

A subtlety worth being explicit about: calling the same custom hook from two different components does not connect them to shared state. Each call gets its own independent useState underneath — exactly like calling a regular function twice gives you two separate local variables, not one shared one.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  return [on, () => setOn((v) => !v)];
}
 
function ComponentA() {
  const [on, toggle] = useToggle(); // its own independent `on`
}
 
function ComponentB() {
  const [on, toggle] = useToggle(); // a completely separate `on`
}

If genuinely shared state across components is what's needed, that's what lifting state up or context (from the previous lessons) are for — a custom hook reuses behavior, not a value.

Try it yourself

Try it yourself
Loading playground...

What to remember

  • A custom hook is a plain function, named starting with use, that calls other hooks to extract reusable stateful logic.
  • Hooks must be called at the top level, in the same order every render — never inside conditions or loops — because React tracks hook state by call order, not by name.
  • The use prefix signals to linters and readers that a function follows the rules of hooks.
  • A custom hook shares logic between components, not state — each call site gets its own independent instance of whatever state the hook holds.

Check yourself

4 questions · pass 3/4 to unlock Prop Drilling and When Context Is the Wrong Fix

up to 50
  1. 1.What is a custom hook, mechanically?

  2. 2.The rules of hooks say hooks must be called at the top level, never inside conditions, loops, or nested functions. Why does this rule exist?

  3. 3.If two different components both call the same custom hook useToggle(), do they share the same state?

  4. 4.Which of these is a valid reason to extract logic into a custom hook rather than leaving it inline in a component?

4 left to answer