Custom hooks
Reusable logic, the rules of hooks, useSyncExternalStore.
Updated
What a hook is
A hook is a special React function (its name starts with use) that gives a component access to React features: memory (useState), effects (useEffect), context (useContext) and so on.
A custom hook is a function you write that uses other hooks — that's how you extract stateful logic and reuse it in several components.
function useToggle(initial = false) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(v => !v), [])
return [on, toggle] as const
}
const [menuOpen, toggleMenu] = useToggle()
const [darkMode, toggleDark] = useToggle(true) // completely separate stateImportant: a custom hook shares logic, not state. Every component that calls it gets its own copy.
The rules of hooks
- Call hooks only at the top level of a component or of another hook — not inside an
if, a loop or a callback. - Only from React components or other hooks — not from regular functions.
Why: React identifies hooks by the order of the calls on every render. An if that skips a hook scrambles the order and the state ends up at the wrong hook.
React's hooks — by category
| Category | Hooks | For |
|---|---|---|
| state | useState, useReducer |
the component's memory |
| context | useContext, use(Context) |
shared data without props |
| effects | useEffect, useLayoutEffect |
synchronizing with the outside |
| references | useRef |
the DOM and values without re-rendering |
| performance | useMemo, useCallback |
memoization (the React Compiler does it automatically) |
| external sources | useSyncExternalStore |
subscribing to stores, media queries, localStorage |
| actions (React 19) | useActionState, useOptimistic, useFormStatus, useTransition |
forms, mutations, optimistic UI |
| async data | use(promise) |
reading a Promise with Suspense |
Examples worth having in any project
function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(id)
}, [value, delay])
return debounced
}
function useMediaQuery(query: string) {
return useSyncExternalStore(
cb => {
const m = matchMedia(query)
m.addEventListener('change', cb)
return () => m.removeEventListener('change', cb)
},
() => matchMedia(query).matches,
() => false, // the value on the server
)
}useSyncExternalStore is the correct way to subscribe to an external source — exactly how the code editor in this app detects the light/dark theme.
When to extract a custom hook
| Sign | Example |
|---|---|
the same useState + useEffect combo in 2+ components |
debounce, fetch, a media query |
| a component has lots of logic and little JSX | useCheckout() takes out the logic, the component stays pure UI |
| you want to test the logic separately | the hook is tested with renderHook |
Don't extract hooks "because you can" — a hook used once, with 3 lines, only hides the code.
Where they live in FSD
A hook tied to an entity → entities/<x>/model/use-x.ts. A generic hook (debounce, a media query) → shared/lib/.
Summary
- A custom hook = a
use...function that composes other hooks; it shares logic, not state. - Only at the top level, always in the same order.
useSyncExternalStorefor external sources; extract hooks when logic repeats.