Why We May Not Need useEffect Anymore — The New Era of React Without Overengineering

Why We May Not Need useEffect Anymore — The New Era of React Without Overengineering
Photo by carlhauser / Unsplash

Introduction

For years, useEffect has been the Swiss Army knife of React developers — the go-to tool for everything from fetching data to synchronizing component state. But over time, this once-beloved hook became something else: a crutch. Developers reached for it instinctively, even when they didn’t need to.

With the arrival of React 18, React Server Components (RSC), and new data-handling paradigms in frameworks like Next.js, a quiet revolution began. Many of the tasks we once solved with useEffect can now be expressed more declaratively, efficiently, and cleanly.

Let’s unpack why this shift matters, what it means for modern React development, and how to adopt this mindset without losing your sanity.


1. The Original Purpose of useEffect

The intent of useEffect was simple:

Allow developers to run side effects in function components.

A side effect means something that affects the outside world — a network request, an event listener, a timer, or a DOM mutation.

Example:

useEffect(() => {
  document.title = `Welcome, ${user.name}`;
}, [user.name]);

This is a valid use: it interacts with the DOM and updates it after React renders.

But over time, useEffect became a catch-all mechanism for logic that didn’t belong there. Many developers started using it for:

  • Data fetching
  • State synchronization
  • Derived computations
  • Even conditionally rendering data

And that’s where the trouble began.


2. The Overuse Problem

React’s core philosophy is declarative programming — describe what the UI should look like given a certain state, and let React handle how to render it.

When we shove business logic or data synchronization into useEffect, we break that model. The component becomes imperative, harder to reason about, and prone to race conditions and infinite loops.

Example of misuse:

const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${user.firstName} ${user.lastName}`);
}, [user]);

This looks innocent, but it’s unnecessary. The full name is derived state, which can be computed directly in render:

const fullName = `${user.firstName} ${user.lastName}`;

No effect needed.
No extra state.
No synchronization bugs.


3. Data Fetching Has Moved On

Once upon a time, the canonical way to fetch data was inside useEffect:

useEffect(() => {
  fetch('/api/user')
    .then(res => res.json())
    .then(setUser);
}, []);

But this approach triggers waterfall loading and layout shifting, because the component renders before the data arrives.

Modern React solves this through Server Components and framework-level data fetching (e.g., Next.js App Router):

export default async function Page() {
  const user = await getUser();
  return <Profile user={user} />;
}

The component can now load with data already available, no useEffect, no loading flicker, and far better performance.

Data fetching now belongs to the server layer, not the client runtime.


4. Derived State Should Stay Derived

A common smell in React code is using useEffect to update state that depends on other state.

Bad pattern:

const [filtered, setFiltered] = useState([]);
useEffect(() => {
  setFiltered(items.filter(i => i.active));
}, [items]);

Better:

const filtered = items.filter(i => i.active);

React’s rendering model already recalculates during updates. Adding effects introduces double rendering and unnecessary complexity.


5. The Rise of useSyncExternalStore and Reactive Stores

With React 18’s concurrent rendering, some patterns that previously required useEffect now have safer alternatives.

Example: subscribing to an external store.

Old way:

useEffect(() => {
  const unsub = store.subscribe(() => setValue(store.get()));
  return unsub;
}, []);

New way:

const value = useSyncExternalStore(store.subscribe, store.get);

This hook ensures React’s internal state remains synchronized with external data sources safely, even during concurrent rendering.

Modern libraries like Zustand, Jotai, and Valtio also build on this foundation, offering more predictable, effect-free subscriptions.


6. Ref-Based Side Effects, Not Effect-Based Refs

Another area where developers lean too much on useEffect is DOM manipulation:

useEffect(() => {
  inputRef.current.focus();
}, []);

Modern JSX allows for direct ref callbacks:

<input ref={el => el?.focus()} />

Cleaner, shorter, and runs exactly when the element mounts — without introducing a full effect cycle.


7. Where useEffect Still Makes Sense

Despite all this, useEffect isn’t dead. It still has its place in true side effects:

  • Subscribing or unsubscribing from WebSocket or event listeners
  • Running analytics or logging after user actions
  • Timers, intervals, or animations
  • Interfacing with third-party libraries that depend on DOM presence

The rule of thumb is:

If it affects something outside of React’s render cycle, keep it in useEffect.

8. The Mental Model Shift

The deeper idea is that React today encourages developers to think declaratively. Rather than orchestrating when things happen, describe what the UI should represent at any point in time.

This change means:

  • Data fetching should happen outside React or at the framework level.
  • State should represent what’s needed, not mirror what already exists.
  • Effects should be rare, explicit, and about the “real world.”

As Dan Abramov once phrased it:

“Most of your components should be pure — effects are the exception, not the rule.”

9. Common Migration Opportunities

Here are patterns worth revisiting in existing projects:

Old (with useEffect) Better Modern Approach
Fetching in useEffect Fetch server-side (Next.js / RSC) or use SWR/React Query
Syncing props to state Derive directly in render
DOM focus in useEffect Use ref callback
Custom store subscription Use useSyncExternalStore
Memoized calculations Use useMemo (if expensive)
Analytics after render Keep as effect — valid side effect

10. Other Considerations

  • React Query / SWR already abstract away the side effects of data fetching. They use internal effects correctly so you don’t have to manage them.
  • React Server Components (RSC) allow zero-client-code fetching, improving performance and removing entire layers of useEffect logic.
  • Transitions and Suspense handle async rendering declaratively — no need for “loading” logic inside effects.
  • Framework conventions (Next.js, Remix, etc.) are aligning around this: your async data belongs on the server, your UI state belongs in components, and your effects belong to the real world.

11. The Philosophy Moving Forward

React is moving toward a world where:

  • Most logic is synchronous, declarative, and server-driven
  • useEffect becomes the last resort, not the first instinct
  • UI and data fetching are separated by responsibility

The end goal?
Simpler, faster, and more predictable React code.

If you find yourself writing useEffect for something that doesn’t involve a real side effect, it’s a hint:

There’s probably a cleaner way.

Finally

“We may not need useEffect anymore” doesn’t mean the hook is obsolete. It means we’re finally learning to respect its purpose.

React’s evolution — from client-only rendering to server-aware frameworks — gives us better tools to write pure, predictable, and declarative code.

So next time your hands start typing useEffect(() => { ... }, []), pause and ask:

Is this truly a side effect? Or am I just compensating for missing architecture elsewhere?

That small moment of reflection can turn spaghetti logic into elegant, maintainable React — the kind the React team envisioned all along.

Support Us

Share to Friends