React Hooks Guide (2026): useRef & useReducer
React Hooks let function components use state, effects, refs, context, and memoization. The API is small. The design decisions are not.
This guide explains the Hooks that matter most in production React code. It focuses on when to use each Hook, when not to use it, and how to avoid common performance and accessibility problems.
Quick Guide
| Hook | Use it for | Do not use it for |
|---|---|---|
useState | Values that affect rendered output | Mutable values that should not render |
useRef | DOM references and persistent mutable values | Visible state changes |
useReducer | Related state transitions | One simple boolean or input |
useEffect | Synchronizing with external systems | Derived values during render |
useMemo | Measured expensive calculations | Every calculation by default |
useCallback | Measured function identity needs | Correctness or habit |
| Custom Hook | Reusable stateful behavior | Sharing unrelated UI markup |
Rules Of Hooks
Call Hooks only at the top level of a function component or custom Hook. Do not call them inside conditions, loops, nested functions, or event handlers.
React relies on stable call order to match Hook state with the correct call. This is why the following pattern is unsafe:
if (enabled) {
const [value, setValue] = useState("");
}
Move the condition inside the Hook or render a separate component with its own Hook sequence.
useState
Use useState for values that affect the UI:
const [query, setQuery] = useState("");
return <input value={query} onChange={(event) => setQuery(event.target.value)} />;
When the next value depends on the previous value, use the functional form:
setCount((current) => current + 1);
This avoids stale values when several updates are queued together.
State ownership
Keep state at the lowest component that needs it. Lift state only when multiple components need the same source of truth. This keeps updates local and reduces unnecessary rendering.
useRef
useRef stores a value that persists between renders. Updating ref.current does not cause a render.
DOM reference
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus();
}
Previous value
const previousQuery = useRef(query);
useEffect(() => {
previousQuery.current = query;
}, [query]);
Do not use a ref when the value must appear in the rendered output. Use state for visible values.
useReducer
Use useReducer when state transitions have related rules:
type State = { status: "idle" | "loading" | "success" | "error" };
type Action = { type: "submit" } | { type: "success" } | { type: "error" };
function reducer(state: State, action: Action): State {
if (action.type === "submit") return { status: "loading" };
if (action.type === "success") return { status: "success" };
return { status: "error" };
}
The reducer is a pure function. This makes transition rules easy to test.
Use useState when the state model is simpler than the reducer.
useEffect
An effect synchronizes React with something outside React:
- Browser events
- Timers
- Subscriptions
- Network clients
- Third-party widgets
useEffect(() => {
const onOnline = () => setOnline(true);
window.addEventListener("online", onOnline);
return () => window.removeEventListener("online", onOnline);
}, []);
The cleanup function must remove the external subscription or cancel the external work.
Do not use effects for derived data
Avoid this:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Use a derived value during render:
const fullName = `${firstName} ${lastName}`;
This removes an extra render and avoids state getting out of sync.
useMemo And useCallback
Use useMemo for an expensive calculation that has a measured performance cost:
const filteredItems = useMemo(
() => items.filter((item) => item.name.includes(query)),
[items, query],
);
Use useCallback when a stable function identity is needed by a memoized child or a dependency:
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
Do not add either Hook to every component. They add code and can make dependencies harder to understand.
Custom Hooks
A custom Hook shares stateful behavior, not rendered markup:
function useDocumentTitle(title: string) {
useEffect(() => {
document.title = title;
}, [title]);
}
Use a custom Hook when the same behavior appears in multiple components or when it makes one component easier to read.
Accessibility With Hooks
Hooks do not make a component accessible automatically. When a Hook updates dynamic content:
- Keep labels connected to controls.
- Preserve keyboard focus after state changes.
- Use
aria-livefor important asynchronous status. - Use
aria-invalidandaria-describedbyfor validation errors. - Do not hide the only error message inside a visual animation.
Use the Accessibility Quick Audit to check the final page.
Performance With Hooks
Measure before optimizing. Check:
- Render frequency
- Expensive calculations
- Large lists
- JavaScript bundle size
- Network waterfalls
- Long tasks
Use the Bundle Risk Analyzer to inspect dependency cost and the Page Speed ROI Calculator to connect performance work to business assumptions.
Common Mistakes
Putting every value in state
Derived values should usually be calculated during render. Extra state creates synchronization problems.
Missing effect cleanup
Listeners, timers, and subscriptions must be cleaned up. Otherwise, old work continues after a component changes or unmounts.
Using a ref for visible state
The UI will not update when ref.current changes. Use state when the value belongs in the render output.
Adding memoization without measurement
Memoization is not free. It adds comparison work and more dependency rules.
Ignoring invalid states
Loading, empty, error, and success states are part of the component contract. Model them explicitly instead of using several loosely related booleans.
Final Checklist
- Is state owned by the lowest component that needs it?
- Does every effect synchronize with an external system?
- Does every effect clean up its work?
- Is
useRefused only for non-rendering mutable values or DOM references? - Is
useReducersolving a real state-transition problem? - Was memoization added because of a measured problem?
- Are loading, empty, error, and success states accessible?
- Can keyboard users complete the same flow?
Good Hook usage reduces complexity. It does not add abstraction for its own sake.