Custom Hooks & Patterns
Compose hooks. Compound components. Headless UI.
Once you've written the same useState + useEffect pattern three times, you're ready for the most important React pattern nobody talks about loudly enough: custom hooks. They are the answer to "how do I share stateful logic between components?" In React, the answer is always "write a hook."
The pattern: a function that uses hooks
A custom hook is just a function whose name starts with useand which internally calls other hooks. That's the whole rule.
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
// In a component
function App() {
const { count, increment, reset } = useCounter(10);
return <button onClick={increment}>{count}</button>;
}Each call to useCounter creates its own independent state. Two components calling it get two separate counters. Hooks share logic, not state.
The rules of hooks
- Only call at the top of a component or another hook. Not inside if-statements, loops, or callbacks. React tracks hooks by their order per render. If the order changes, the hook state gets misaligned.
- Only call from React components or other hooks. Not from regular utility functions, not from event handlers, not from effects. The lint rule
react-hooks/rules-of-hookswill yell at you. - Name them
useX. The linter uses the prefix to identify a hook and apply rule #1. Call itfetchDataand the linter won't protect you.
What good custom hooks do
Look for these signals that something should become a hook:
- Multiple components have the same
useState + useEffectdance. - A piece of logic involves a hook AND would otherwise be a utility function.
- You're wiring up a browser API (geolocation, media query, online status).
- You're abstracting a third-party library's side effects.
A useful real-world hook: useLocalStorage
function useLocalStorage(key, initial) {
// Lazy init so we don't read localStorage on every render
const [value, setValue] = useState(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) : initial;
} catch {
return initial;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {}
}, [key, value]);
return [value, setValue];
}
// Same API as useState, but persists to localStorage
const [name, setName] = useLocalStorage("name", "Ada");That's a custom hook earning its keep. You write it once, every component that needs persistence is one line of code, and the complexity (serialization, errors, lazy init) is hidden in one place.
Compound components
A pattern hooks make easy. Instead of one giant component with 20 props, you make a parent component plus child components that communicate via context. Consumers compose them.
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile">...</Tabs.Panel>
<Tabs.Panel value="settings">...</Tabs.Panel>
</Tabs>Internally, Tabs creates a context with the active value and a setter. Trigger and Panel subscribe via useContext. The hook is the glue. The user writes readable JSX.
Headless UI: the new ecosystem default
The biggest shift in React UI in 2026 is "headless" component libraries. Instead of opinionated styled widgets, Radix Primitives and React Aria give you accessible, unstyled, fully-functional building blocks. You style them however you want (typically with Tailwind or your design system). They handle the hard part: keyboard navigation, ARIA, focus traps, screen-reader support.
- Radix Primitives: low-level dialogs, dropdowns, tabs, tooltips. Composable, headless. The shadcn/ui library is built on top.
- React Aria: hooks first. Adobe's industrial-grade a11y library.
useButton,useCheckbox,useDialog. Use the hooks and write your own markup. - shadcn/ui: not a library, a registry. You
npx shadcn add buttonand the source lands in your repo. You own the code.
For a fresh React project in 2026, this is the default. You almost never build a date picker, dialog, or combobox from scratch anymore. You take a tested primitive and style it.
Try it: useLocalStorage in action
Type into the box. Refresh the iframe. Your text comes back. That's a custom hook hiding all the messy details.
Quiz
What makes a function a "hook"?
Recap
- A custom hook is a function named
useSomethingthat calls other hooks. That's it. - Rules of hooks: top of the component or another hook, never inside conditionals/loops. Always called
useX. - Each call creates its own state. Hooks share logic, not state.
- Reach for them when you see the same hook combo in multiple places, or when you're wiring up a browser API.
- Headless UI libraries (Radix, React Aria, shadcn/ui) are the modern baseline. Don't build dialogs from scratch in 2026.