A custom hook is just a function
No new concept, no API to learn: a function whose name starts with use and which calls hooks. The name is what tells React (and the linter) to apply the rules to it.
function useToggle(initial = false) {
const [on, setOn] = React.useState(initial);
const toggle = React.useCallback(() => setOn((v) => !v), []);
return [on, toggle];
}
function Panel() {
const [open, toggle] = useToggle();
return <button onClick={toggle}>{open ? "Close" : "Open"}</button>;
}
It shares logic, not state
Two components calling useToggle() get two independent pieces of state. A hook is a recipe, not a store. If two components must share one value, lift it up or put it in a context.
The two rules
State lives in a list attached to the component instance. React walks that list in order on every render, which is why the order has to be identical every time.
- Call hooks at the top level. Never inside an
if, a loop, a nested function or after an earlyreturn. - Call them only from a component or another hook. Not from an ordinary function, not from an event handler.
// React identifies each hook by its CALL ORDER, not by name
if (user) {
const [name, setName] = useState(""); // sometimes the 1st hook, sometimes not
}
const [age, setAge] = useState(0); // now reads the wrong slot
// the fix is always the same: move the condition inside
const [name, setName] = useState("");
if (user) { … }
What to return
// an array, so the caller can name them freely — like useState
const [open, toggle] = useToggle();
const [value, setValue] = useLocalStorage("key", "");
// an object, once there are three or more
const { count, increment, decrement, reset } = useCounter(0);
const { data, error, isPending } = useDoors(query);
What is worth extracting
Not worth extracting: a single useState with a nice name, or a wrapper that only renames arguments. A hook with five options and three return values is usually two hooks.
- Logic used in more than one component — a media query, a debounce, a subscription.
- A block of hooks that has one job and a name:
useOnlineStatus,useClickOutside,useInterval. - Anything that makes a component hard to read — an effect with a subscription and cleanup, sitting in the middle of markup.
A hook with an effect
The cleanup is the entire mechanism: each render cancels the previous timer, so the state is only set once the value has stopped changing.
function useDebounced(value, delay = 300) {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id); // a new keystroke cancels the pending one
}, [value, delay]);
return debounced;
}
const query = useDebounced(text, 300); // only settles when typing stops
Naming
A good hook name says what the component gets, not when something happens. If the only honest name is useEffectStuff, the code probably belongs where it is.
| Name | Reads as |
|---|---|
useOnlineStatus() | a value you are given |
useClickOutside(ref, fn) | a behaviour you attach |
useDoors(query) | data for this component |
useMount(fn) | avoid — it describes a lifecycle, not a purpose |
Before writing one
React ships useId for stable ids, useSyncExternalStore for subscribing to anything outside React, useDeferredValue and useTransition for keeping an interface responsive. Libraries ship the rest. Check first — useDebounce has been written ten thousand times.