Lesson 19 of 31
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.
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:
- Only call hooks at the top level — never inside a condition, loop, or nested function.
- 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
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
useprefix 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
1.What is a custom hook, mechanically?
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.If two different components both call the same custom hook
useToggle(), do they share the same state?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