Skip to main content

Custom Hooks and Patterns

Writing Custom Hooks

Written by Published

A custom hook is not a React feature. It is a function whose name starts with use and which calls other hooks - that convention is the entire mechanism.

Extracting logic, not markup

Components share markup. Hooks share behaviour. When two components need the same stateful logic but look nothing alike, a hook is what you want.

From duplicated logic to one hook

jsx

// The hook: all the logic, none of the markup.
function useToggle(initial = false) {
  const [on, setOn] = React.useState(initial)
  const toggle = () => setOn((v) => !v)
  return [on, toggle]
}

function Panel() {
  const [open, toggleOpen] = useToggle(true)
  return (
    <div>
      <button onClick={toggleOpen}>{open ? "Collapse" : "Expand"}</button>
      {open && <p>Panel body</p>}
    </div>
  )
}

function Switch() {
  const [on, toggleOn] = useToggle()
  return (
    <p>
      <label>
        <input type="checkbox" checked={on} onChange={toggleOn} /> {on ? "On" : "Off"}
      </label>
    </p>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<div><Panel /><Switch /></div>)

Two components with completely different markup share one piece of behaviour. Neither knows the other exists.

Each caller gets its own state

A hook is not a shared store. Calling it twice creates two independent pieces of state, exactly as writing useState twice would. What is shared is the code, not the values.

Two callers, two states

jsx

function useCounter(start = 0) {
  const [count, setCount] = React.useState(start)
  return { count, add: () => setCount((c) => c + 1) }
}

function Widget({ label }) {
  const { count, add } = useCounter()
  return <button onClick={add}>{label}: {count}</button>
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <div style={{ display: "flex", gap: 8 }}>
    <Widget label="First" />
    <Widget label="Second" />
  </div>
)

A hook that owns an effect

The real payoff is hiding a subscription and its cleanup, so callers get a value and never have to remember to tear anything down.

useWindowWidth

jsx

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
}

function Layout() {
  const width = useWindowWidth()
  return (
    <p>
      Width is {width}px — {width < 700 ? "narrow" : "wide"} layout.
      Resize the preview panel to see it change.
    </p>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Layout />)

The rules of hooks

  1. Only call hooks at the top level - never inside a condition, loop or nested function.
  2. Only call them from a component or another hook.

React matches hooks to their stored values by call order, not by name. A hook inside an if changes that order between renders, and React ends up handing your state to the wrong hook.

What to return

Return an array when the caller will usually rename the values, as useState does - array destructuring makes renaming trivial. Return an object when there are several values and callers may want only some of them.

Two values that pair naturally, like a value and its setter, suit an array. Four values where a caller might take one suit an object, because const { width } = useSize() reads better than counting positions.

  • Name it useSomething - the linter relies on it.
  • Return an array for one or two paired values, an object for more.
  • Each call is independent state.
  • Extract when logic repeats, not speculatively.