Custom Hooks

Course 2 · Ch 4
Custom Hooks
Extracting reusable stateful logic into a function of your own — exactly what useTheme already did, generalized

Chapter 2's useTheme was already a custom hook, introduced without dwelling on the concept itself. A custom hook is nothing more than a regular JavaScript function whose name starts with use, which calls other hooks inside it — there's no special syntax, no registration step, no library involved. It exists purely to pull repeated stateful logic (state plus the behavior around it) out of components and into something reusable, the same way a regular function extracts repeated non-stateful logic.

The Simplest Possible Custom Hook

function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); function toggle() { setValue((v) => !v); } return [value, toggle]; } // usage — looks just like useState, but with toggle behavior built in function Switch() { const [isOn, toggleIsOn] = useToggle(); return <button onClick={toggleIsOn}>{isOn ? "On" : "Off"}</button>; }

useToggle wraps a useState call and the boolean-flip logic that goes with it (the same pattern as Fundamentals Chapter 3's LightSwitch), so any component needing toggle behavior can call one hook instead of writing the flip function itself every time. It returns a tuple, like useState does, purely as a convention — a custom hook can return anything at all (an object, a single value, several values).

useLocalStorage — Syncing State with the Browser

function useLocalStorage(key, initialValue) { const [value, setValue] = useState(() => { const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : initialValue; }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue]; } // usage — drop-in replacement for useState, now persisted automatically const [notes, setNotes] = useLocalStorage("notes", []);

This is exactly the load/save pair Project 3's stretch goals suggested extracting — two effects' worth of localStorage logic, now living in one hook instead of being duplicated in every component that needs persistence. Passing a function to useState (rather than a plain value) is worth noting here: that initializer function only runs once, on the very first render, rather than re-reading localStorage on every single re-render.

useDebounce — Delaying a Fast-Changing Value

function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timeoutId = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timeoutId); }, [value, delay]); return debouncedValue; } // usage const [query, setQuery] = useState(""); const debouncedQuery = useDebounce(query, 400); // fetch only when debouncedQuery changes, not on every keystroke in query

This is the Book Search project's debounce logic, lifted out of a component and into a reusable hook — exactly the stretch goal it suggested. useDebounce takes a fast-changing value (query, updated on every keystroke) and returns a second value that only catches up after delay milliseconds of no further changes, using the same setTimeout + cleanup pattern from Fundamentals Chapter 8 and Project 4, just packaged for reuse anywhere a debounced value is needed.

When something deserves to become a custom hook
Not every reusable bit of logic needs to be a hook — a plain helper function is the right tool when no state or other hook is involved at all. The signal for a custom hook specifically is reusable logic that depends on useState, useEffect, useContext, or another hook internally. If two or more components are duplicating the same stateful behavior, that's usually the moment to extract it.
Custom hooks still follow the rules of hooks
Because a custom hook calls other hooks internally, it inherits the exact same rules from Fundamentals Chapter 3: call it only at the top level of a component (or another hook) — never inside a condition, loop, or nested function. The use prefix isn't just a naming convention; tools like the ESLint hooks plugin specifically look for that prefix to know which functions to apply these rules to.

Coding Challenges

Challenge 1

Write the useToggle hook from this chapter and use it in two different components — one toggling a sidebar's open/closed state, one toggling a "show password" boolean on a password input.

📄 View solution
Challenge 2

Write the useLocalStorage hook from this chapter, and use it to persist a simple counter's current value, confirming it survives a page refresh.

📄 View solution
Challenge 3

Write the useDebounce hook from this chapter. Build a search input where the raw value updates on every keystroke, and a separate debounced value (400ms) is displayed below it once typing pauses — enough to see the two values catch up to each other after a short delay.

📄 View solution

Chapter 4 Quick Reference

  • A custom hook is just a function starting with use that calls other hooks inside it
  • Extract one when several components duplicate the same stateful logic — not just any repeated code
  • useToggle — boolean state + a flip function, wrapped for reuse
  • useLocalStorage — a drop-in useState replacement that persists automatically
  • useDebounce — returns a value that only catches up after a pause in changes
  • Custom hooks still follow the rules of hooks — top-level calls only, no conditionals
  • Next chapter: React Router — multiple pages in a single-page app, first previewed in Project 4