Extract reusable stateful logic, such as toggles and filters, into your own use-prefixed hooks.
Why it matters
After a few components you will notice the same patterns repeating: a boolean that toggles, a search box that filters a list, a value synced to the window size. Custom hooks let you pull that stateful logic out of components into a function you can reuse and test, the same way you extract a helper function from repeated code.
A custom hook is a function that uses hooks
Here is a toggle written inline:
function Menu() {
const [open, setOpen] = React.useState(false);
return <button onClick={() => setOpen((o) => !o)}>{open ? "Close" : "Open"}</button>;
}
And as a custom hook:
function useToggle(initial = false) {
const [on, setOn] = React.useState(initial);
const toggle = () => setOn((v) => !v);
return [on, toggle];
}
function Menu() {
const [open, toggleOpen] = useToggle();
return <button onClick={toggleOpen}>{open ? "Close" : "Open"}</button>;
}
Rules and conventions:
- The name must start with
use. That is how React and its lint rules know the rules of hooks apply. - A custom hook can call
useState,useEffect,useReducer,useContextand other custom hooks. - It follows the same rules: call it at the top level of a component or another hook, never inside conditions or loops.
- It can return anything: a value, an array like
useState, or an object.
Hooks share logic, not state
Each component that calls useToggle gets its own independent state. Two menus using the hook do not open together. A custom hook is a way to reuse how state works, not a shared store. To share the same value across components you lift state up or use context (next lesson).
Returning an array or an object
- Return an array when there are one or two values and callers will want their own names:
const [isOpen, toggleOpen] = useToggle();. - Return an object when there are several values: callers pick what they need by name, and you can add fields later without breaking anyone.
A filter hook
Filtering a list by a search term appears in almost every app:
function useFilter(items, getText) {
const [query, setQuery] = React.useState("");
const needle = query.trim().toLowerCase();
const results =
needle === ""
? items
: items.filter((item) => getText(item).toLowerCase().includes(needle));
return { query, setQuery, results };
}
function ProductSearch({ products }) {
const { query, setQuery, results } = useFilter(products, (p) => p.name);
return (
<>
<input
aria-label="Search products"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<p>{results.length} result(s)</p>
<ul>
{results.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</>
);
}
Notice that the hook stores only query in state and derives results during render. Passing getText makes it work for any kind of item. For very large lists you could wrap the filter in React.useMemo(() => ..., [items, needle]) so it only recomputes when its inputs change, but measure before optimising.
A hook with an effect
Hooks can also wrap effects, hiding the subscribe and cleanup details:
function useWindowWidth() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return width;
}
Any component can now write const width = useWindowWidth(); without knowing about event listeners. A data-loading hook such as useFetch(url) that returns { data, status } is another common example, built from the effect pattern in the previous lesson.
Stable functions with useCallback
A function created inside a hook is a new function on every render. That rarely matters, but if a caller puts it in an effect's dependency list, the effect would re-run each time. React.useCallback(fn, deps) returns the same function until the dependencies change:
const toggle = React.useCallback(() => setOn((v) => !v), []);
Using the updater form (v => !v) means the callback does not depend on on, so its dependency list can be empty.
Testing hooks
Hooks only run inside components, so tests render a tiny component that calls the hook and exposes its result. Testing Library provides renderHook for exactly this. The Do exercise's checks do the same thing by hand.
When not to make a hook
Do not wrap a single useState call in a hook just for the sake of it, and do not turn plain calculations into hooks. If a function does not call any hooks, make it a regular function (formatPrice, sortByDate): it can then be called anywhere, including inside loops and conditions.
Common mistakes
- Naming the function without
use, which hides rule-of-hooks bugs from the linter. - Expecting two components using the same hook to share state.
- Calling a hook conditionally:
if (enabled) useToggle(). - Storing derived results in state inside the hook instead of computing them.
Recap
- A custom hook is a
use...function that calls other hooks to package stateful logic. - Every caller gets its own state; hooks share behaviour, not data.
- Return arrays for one or two values, objects for more; derive what you can.
- Hooks can wrap effects and their cleanup; plain logic should stay a plain function.
Try it
Open the Do tab to write useToggle and useFilter, then watch the fruit picker that uses them come to life.