What are React hooks and how do you create custom hooks?
Hooks are functions starting with `use` that let function components opt into React features (state, effects, context, refs). A custom hook is just a function that calls other hooks — naming convention `useXxx` enables the linter to enforce the rules of hooks. Extract reusable stateful logic by composing built-in hooks into a function.
Hooks let function components do everything class components could, plus reuse stateful logic without HOCs or render props.
Built-in hooks
| Hook | Purpose |
|---|---|
useState | Local component state |
useEffect | Side effects after render |
useContext | Subscribe to context |
useRef | Mutable container; DOM access |
useMemo | Memoize a computed value |
useCallback | Memoize a function reference |
useReducer | Complex state via reducer |
useTransition | Defer non-urgent updates |
useDeferredValue | Lag a value during heavy work |
useId | Stable IDs across server/client |
useSyncExternalStore | Subscribe to an external store |
Rules of hooks
- Only call at the top level — never inside conditionals, loops, or nested functions.
- Only call from React functions — components or other hooks.
These rules let React track hooks by call order across renders. The ESLint plugin eslint-plugin-react-hooks enforces both.
Creating a custom hook
A custom hook is any function whose name starts with use and which calls other hooks.
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const inc = useCallback(() => setCount(c => c + 1), []);
const dec = useCallback(() => setCount(c => c - 1), []);
const reset = useCallback(() => setCount(initial), [initial]);
return { count, inc, dec, reset };
}Use it:
function App() {
const { count, inc } = useCounter();
return <button onClick={inc}>{count}</button>;
}More examples
useDebounced
function useDebounced<T>(value: T, ms = 300) {
const [v, setV] = useState(value);
useEffect(() => {
const id = setTimeout(() => setV(value), ms);
return () => clearTimeout(id);
}, [value, ms]);
return v;
}useOnlineStatus (built around an external store)
function useOnlineStatus() {
return useSyncExternalStore(
cb => {
window.addEventListener('online', cb);
window.addEventListener('offline', cb);
return () => {
window.removeEventListener('online', cb);
window.removeEventListener('offline', cb);
};
},
() => navigator.onLine,
() => true, // SSR fallback
);
}Why hooks replaced HOCs/render-props
- No 'wrapper hell' in the React DevTools tree.
- Composable: hooks call hooks, naturally.
- TypeScript inference works without ceremony.
- Logic and UI separate cleanly: hook = logic, component = view.
When NOT to make a custom hook
If the logic isn't reused, inlining is fine. Extract when you have the second or third caller.
Follow-up questions
- •Why do hooks have to be called at the top level?
- •How does React know which state belongs to which hook call?
- •When would you use useReducer over useState?
Common mistakes
- •Calling hooks conditionally — breaks call-order tracking.
- •Naming a custom function `getCounter` instead of `useCounter` — linter can't enforce rules.
- •Building a custom hook for a single caller, before you know the abstraction is right.
Performance considerations
- •Custom hooks compile to plain function calls — no runtime overhead beyond the underlying hooks they invoke. The cost model is the same as if the logic were inlined.
Edge cases
- •Returning unstable references that consumers put in deps — infinite re-renders.
- •SSR: useSyncExternalStore needs a server snapshot to avoid hydration mismatches.
- •StrictMode dev double-mount surfaces hooks that aren't idempotent.
Real-world examples
- •Libraries built around custom hooks: TanStack Query (`useQuery`), React Hook Form (`useForm`), Zustand (`useStore`). The model has won.