·5 min read

The death of useEffect: what to use instead in 2026

Server components, server actions, useOptimistic, useSyncExternalStore. The hooks that quietly replaced the universal escape hatch, and the one job useEffect still has.

ReactHooksPatterns

For most of React's history, useEffect was the universal escape hatch. Data fetching, subscriptions, syncing with browser APIs, even basic state derivations got bolted onto effects because the rest of the API surface was too small. In 2026 that is no longer true. The right answer to most things I used to reach for an effect is now a different primitive, and the codebases I work on are quieter for it.

Data fetching

Server components fetch directly. No effect, no loading state, no race-condition guards. The data is part of the render, returned from an async component, streamed in via Suspense. The mental shift here is the biggest one: data fetching stops being a side effect and becomes a property of the tree.

For client components, the answer is usually a framework data hook (TanStack Query, SWR, the Next.js cache) wrapping the actual fetch. These libraries handle the things effects always handled badly: deduplication, retry, cache invalidation, optimistic updates. Writing a fresh useEffect(() => fetch(url).then(setData), [url]) in 2026 is almost always a sign you skipped a step.

Form submissions and mutations

Server actions and useActionState killed the effect-driven submit pattern. You write a server function, pass it to <form action={...}>, and get pending state, error handling, and progressive enhancement for free. The form works without JavaScript. The optimistic update is one extra line with useOptimistic.

The pattern I used to write (button click, set loading state, await fetch, set result, clear loading, catch and set error) was always 15 lines of bookkeeping around 2 lines of intent. That ratio is now inverted.

Derived state

The classic anti-pattern (useEffect watching one piece of state and calling setState on another) was wrong even in 2018, and people kept writing it. useMemo and plain expressions during render handle this. If the derivation is expensive, useMemo. If it is not, just compute it inline. Either way, no effect.

Subscriptions and external stores

useSyncExternalStore exists and is the right hook for subscribing to anything outside React (Zustand, Redux, browser APIs like window.matchMedia). It handles concurrent rendering correctly. An effect that reads the current value of a store and copies it into local state is wrong: it ships a stale render on the first paint.

What useEffect is still for

Imperative DOM interactions that React does not own: focusing an input on mount, scrolling a container to a position, registering a non-React event listener on document or window. Talking to an imperative library (a chart, a map, a video player) that needs a real DOM node. Cleanup of those registrations.

That list is short. In most components I write today, the count of useEffect calls is zero. Two years ago it was three or four. Nothing about the components got worse. The framework grew up.