Custom hooks in practice
The Hooks chapter introduced the idea: a custom hook is a function that starts with use and composes hooks into reusable logic. This chapter is about what that looks like in practice, and the course makes the point dramatically: after a whole section spent building a headless Toggle component, a custom hook replaces it in a fraction of the code, and the component gets deleted.
The line worth drawing precisely is against an ordinary utility function. A function that fetches data or formats a date is a utility. A custom hook calls other hooks, which is why the rules of hooks apply inside it.
Extracting an effect: useEffectOnUpdate
The Toggle component ended up with a chunk of logic whose job was "run this callback when the state changes, but never on the first render": a useEffect guarded by a firstRender ref. That's not toggle logic; it's effect logic, useful anywhere. Extracting it produces a hook with the same signature as useEffect itself:
export default function useEffectOnUpdate(effectFunction, deps) {
const firstRender = useRef(true)
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
effectFunction()
}
}, deps)
}Callers write useEffectOnUpdate(onToggle, [on]) and the first-render bookkeeping disappears from their code. Hooks that wrap other hooks this way are sometimes worth building even when the wrapped logic is a few lines, because the name carries the intent.
The guard has a development-only wrinkle
The same one render props flagged: StrictMode spends the ref on its first effect run, so the callback fires on the second. Production builds do not do this, and the durable fix is to compare the value against the previous one rather than track whether this is the first render.
Extracting state: useToggle
useEffectOnUpdate returns nothing, like useEffect. Most custom hooks give something back, like useState does, and the shape of what they return is a design decision. useToggle gathers the boolean, the flip function, and the change callback into one place:
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
const [on, setOn] = useState(initialValue)
const toggle = () => setOn(prevOn => !prevOn)
useEffectOnUpdate(onToggle, [on])
return [on, toggle]
}const [on, toggle] = useToggle({ initialValue: true })Each call to useToggle gets its own useState, so two callers hold two independent booleans, whether that's two components or one component calling the hook twice.
Three decisions in there, each a pattern you'll reuse:
- Return an array when the values are a natural pair the caller will rename, exactly like
useState: one caller destructures[on, toggle], another[open, toggleOpen]. Return an object when there are several values and names matter more than order. - Take a configuration object instead of positional parameters. With
useToggle(false, callback)a caller who only wants the callback is forced to supply the boolean first; with{ initialValue, onToggle }they pass what they mean and skip the rest, and every option can carry a default. - Default the callback to a noop (
() => {}) so the hook can callonToggle()unconditionally without crashing callers who never passed one.
The payoff: deleting the component
With useToggle in hand, the star widget stops needing any Toggle machinery at all:
export default function Star({ onChange }) {
const [on, toggle] = useToggle({ onToggle: onChange })
return on
? <BsStarFill className="star filled" onClick={toggle} />
: <BsStar className="star" onClick={toggle} />
}The menu keeps its compound component shape, because its pieces still need shared implicit state, but its provider now gets its values from the hook: Menu calls useToggle, passes { open, toggleOpen } through its own context, and the headless Toggle component's files get deleted.
That deletion is the section's real lesson. The headless component earned its keep while many compound components might have shared it; once the logic fit in a hook, the hook was the simpler tool, and ripping out working code you were proud of is a normal part of the job.
The two patterns are complements: a hook shares logic, a headless component shares logic through the tree via context and composition. Reach for the hook first, and let a component earn the extra ceremony. The course caps this section with a solo project, Component Library++, which puts every pattern from these five chapters into one library.
use and that builds on hooks like useState to package up a piece of logic. Once useToggle exists, any component can get an on-off value and a flip function in one line, the same way useState hands you a value and a setter. Each component that calls it gets its own copy of the state, so a hook is a recipe you reuse rather than a value everyone shares.
Next up: Routing, where the URL starts deciding which components render.

