Writing effective React hooks patterns in 2026 means unlearning a few habits from the last five years. The React 19 era added hooks that absorb work you used to do manually, the React Compiler removed most reasons to sprinkle useCallback everywhere, and the community has converged on clearer rules for what belongs in a custom hook. This post covers the patterns that hold up in real codebases: mutation state with useActionState, instant feedback with useOptimistic, and the discipline that separates good custom hooks from clever ones.
useActionState: the end of hand-rolled mutation state
Most React apps contain the same ritual dozens of times: a submit handler with setLoading(true), a try/catch that stores an error message, and a finally that resets the flag. useActionState collapses that ritual into a single hook. You give it an async reducer that receives the previous state and the submitted FormData, and it returns the current state, a wrapped action for your form, and a pending flag:
function Newsletter() {
const [state, formAction, isPending] = useActionState(
async (prev, formData) => {
const email = formData.get('email');
if (!email.includes('@')) {
return { ok: false, message: 'Enter a valid email.' };
}
await subscribe(email);
return { ok: true, message: 'Check your inbox!' };
},
{ ok: false, message: '' }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button disabled={isPending}>
{isPending ? 'Subscribingβ¦' : 'Subscribe'}
</button>
{state.message && <p role="status">{state.message}</p>}
</form>
);
}
Two patterns make this hook shine. First, treat the returned state as a small discriminated result object, something like { ok, message, fieldErrors }, rather than a bare string. Your JSX stays declarative and you get server-side validation errors for free when the action runs on the server. Second, because the reducer receives the previous state, multi-step flows such as wizards can accumulate progress across submissions without extra useState calls.
Pair it with useFormStatus when the submit button lives in a shared design-system component. The button reads const { pending } = useFormStatus() from the nearest parent form, so every form in the app gets a consistent pending style with zero prop drilling.
useOptimistic: instant UI that cleans up after itself
Optimistic updates used to be the riskiest code in a frontend: update local state immediately, fire the request, and write careful rollback logic for failure. useOptimistic makes the rollback automatic. It gives you a piece of state that shows an optimistic value while an action is pending and snaps back to the canonical value when the action settles:
function Thread({ messages, sendMessage }) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(current, newText) => [
...current,
{ id: 'temp', text: newText, sending: true },
]
);
async function formAction(formData) {
const text = formData.get('message');
addOptimistic(text);
await sendMessage(text);
}
return (
<form action={formAction}>
{optimisticMessages.map((m) => (
<p key={m.id}>
{m.text} {m.sending && <small>(sending)</small>}
</p>
))}
<input name="message" />
</form>
);
}
The key mental model: useOptimistic derives from a source of truth you pass in, usually props from a server or a query cache. When the action finishes and fresh data flows down, the optimistic layer disappears. Mark optimistic items visually, keep the update function pure, and never store the optimistic value anywhere else. The hook works best for high-frequency, low-stakes interactions: likes, reordering, sending messages, toggles.
Custom hooks: extract behavior, not lines
The custom hooks that survive refactors share one property: they encapsulate a behavior with a clear contract, not just a block of code that happened to be long. A few rules of thumb consistently produce good ones:
- Name the capability, not the implementation.
useDebouncedValue,useClipboard, anduseIntersectiondescribe what the caller gets.useFetchDataAndSetStatedescribes plumbing and will not survive its second consumer. - Return the smallest useful surface. Prefer a tuple or a small object with stable names. If a hook returns eight properties, it is probably two hooks.
- Accept primitives where possible. Object and array arguments invite unstable references. When you must accept an options object, read the fields you need rather than depending on the whole object.
- Keep effects inside, events outside. A hook can own subscriptions and cleanup, but user-facing callbacks should be passed in, so the consumer decides what happens.
Here is the shape of a well-behaved hook that wraps an external store correctly using useSyncExternalStore, the right primitive for subscribing to anything outside React:
function useMediaQuery(query) {
const subscribe = useCallback(
(callback) => {
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
[query]
);
return useSyncExternalStore(
subscribe,
() => window.matchMedia(query).matches,
() => false // server snapshot
);
}
Reaching for useSyncExternalStore instead of an effect plus useState avoids tearing during concurrent rendering and gives you a natural place to define server-side behavior.
Habits to retire in 2026
Some patterns that were best practice in 2021 are now noise. Wrapping every function in useCallback is unnecessary in projects using the React Compiler, which memoizes automatically when your code follows the Rules of React; keep explicit memoization only for provably expensive computations. Fetching in useEffect with a loading flag remains workable but is the weakest option available; framework loaders, Server Components, or a library like TanStack Query handle caching, races, and revalidation that the effect version silently gets wrong. And useEffect as a general reaction mechanism, where one effect sets state that triggers another effect, is the most common source of tangled components; most of those chains flatten into derived values computed during render.
A checklist for hook-heavy components
- Mutations go through Actions and
useActionState, not ad-hoc handlers with loading flags. - Optimistic UI derives from canonical data via
useOptimistic, never duplicated into separate state. - External systems are bridged with
useSyncExternalStore, wrapped in a named custom hook. - Derived data is computed during render; state stores only what cannot be derived.
- Custom hooks have single responsibilities and interfaces you would be happy to document.
Hooks were always about moving logic out of component trees and into composable functions. With React 19 handling the mechanical parts of async state, the craft that remains is design: choosing the right primitive, naming the behavior honestly, and keeping each hook small enough to reason about at a glance.
Related Service
π» Web Development
Custom websites and web applications built with PHP, Laravel, WordPress, and React β fast, secure, scalable, and tailored to your business goals.
Explore Web Development →