Custom hooks for state

Move repeated state logic into a use* hook with a stable callback.

From frontend-patterns, “Custom Hooks Patterns: State Management Hook”.

Three toggles, one hook

Flip each toggle and watch which lines own it.

AppSame behavior either way
Q3 revenue $482,190
Invoices 214Paid 91%Overdue 7
sidebar: false · dark: false · details: false
Copied per component15 lines
const [sidebar, setSidebar] = useState(false)const toggleSidebar = useCallback(() => {  setSidebar(v => !v)}, []) const [dark, setDark] = useState(false)const toggleDark = useCallback(() => {  setDark(v => !v)}, []) const [details, setDetails] = useState(false)const toggleDetails = useCallback(() => {  setDetails(v => !v)}, [])
With useToggle3 lines
const [sidebar, toggleSidebar] = useToggle()const [dark, toggleDark] = useToggle()const [details, toggleDetails] = useToggle()
export function useToggle(initialValue = false): [boolean, () => void] {
  const [value, setValue] = useState(initialValue)

  const toggle = useCallback(() => {
    setValue(v => !v)
  }, [])

  return [value, toggle]
}

Toggle twice in one click

Set calls per click, then press Run.

setValue(!value)Stale
    value: false
    setValue(v => !v)Correct
      value: false
      const toggleBad = () => setValue(!value)
      const toggle = useCallback(() => setValue(v => !v), [])

      Stable callbacks skip renders

      Type in the box, then press each child's button.

      Inline arrow propNew fn each render
      Parent renders1
      valuefalse
      <MemoChild> renders1
      onToggle={() => setOpen(v => !v)}
      toggle from useToggleSame fn
      Parent renders1
      valuefalse
      <MemoChild> renders1
      onToggle={toggleOpen}

      const MemoChild = React.memo(function Child({ onToggle }: { onToggle: () => void }) {
        return <button onClick={onToggle}>Toggle</button>
      })
      
      function Parent() {
        const [query, setQuery] = useState('')
        const [isOpen, toggleOpen] = useToggle()
        return (
          <>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <MemoChild onToggle={toggleOpen} />
          </>
        )
      }

      Why it matters, pros and cons

      Why it matters

      Copied logic drifts and goes stale. A hook fixes it once, with a stable callback.

      Pros

      • One implementation, one test, many callers.
      • Stable identity keeps React.memo effective.
      • Callers name the tuple: [isOpen, toggleOpen].

      Cons

      • A hook used once is just indirection.
      • useCallback is noise unless identity is compared.
      • Hooks share logic, not state.