DEV Community

Cover image for I Removed 80% of useEffect From Our Codebase — Here's What Happened
Harsh
Harsh

Posted on

I Removed 80% of useEffect From Our Codebase — Here's What Happened

If you've been writing React for a while, you know the feeling.

Something's not working. Data isn't updating. A bug appears out of nowhere.

And somewhere in your codebase, there's a useEffect quietly causing chaos.

Here's the truth senior React developers know: most useEffect calls shouldn't exist.

I went through a mid-sized React project last month and removed 80% of the useEffect calls. The app got faster, the code got cleaner, and the bugs disappeared.

This article shows you exactly what I used instead. 🚀


Why useEffect Gets Overused

When you first learn React, useEffect feels like a superpower.

Need to fetch data? useEffect.
Need to update something when state changes? useEffect.
Not sure what's happening? Throw in a useEffect and hope for the best.

The problem? useEffect was never meant to be your default tool. It was designed for synchronizing with external systems — things like DOM APIs, third-party libraries, and network requests that React doesn't control.

Using it for everything else causes:

  • ❌ Infinite re-render loops
  • ❌ Stale closure bugs
  • ❌ Race conditions in data fetching
  • ❌ Code that's hard to read and debug

Let's fix that.


Alternative 1: useMemo — For Expensive Calculations

The wrong way — using useEffect:

// ❌ Don't do this
const [filteredUsers, setFilteredUsers] = useState([]);

useEffect(() => {
  const result = users.filter(user => user.isActive);
  setFilteredUsers(result);
}, [users]);
Enter fullscreen mode Exit fullscreen mode

What's wrong here? You're using useEffect to set state based on other state. This causes an extra render cycle every single time.

The right way — using useMemo:

// ✅ Do this instead
const filteredUsers = useMemo(() => {
  return users.filter(user => user.isActive);
}, [users]);
Enter fullscreen mode Exit fullscreen mode

useMemo computes the value during render, not after it. No extra render cycle. No state update. Just pure, clean derived data.

When to use useMemo:

  • Filtering or sorting large arrays
  • Complex calculations based on props or state
  • Creating derived data from existing state

💡 Rule of thumb: If you're calling setX inside a useEffect based on other state/props, you almost certainly want useMemo instead.


Alternative 2: React 19's use() Hook — For Data Fetching

React 19 introduced the use() hook, and it changes everything about how we fetch data.

The old way — useEffect for fetching:

// ❌ Classic useEffect fetching — messy
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(data => {
      setData(data);
      setLoading(false);
    })
    .catch(err => {
      setError(err);
      setLoading(false);
    });
}, []);
Enter fullscreen mode Exit fullscreen mode

That's 15+ lines just to fetch data. And it still has race condition issues.

The new way — React 19's use() hook:

// ✅ Clean, modern, and simple
import { use, Suspense } from 'react';

const usersPromise = fetch('/api/users').then(res => res.json());

function UserList() {
  const users = use(usersPromise);

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// Wrap with Suspense in parent
function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserList />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

No loading state. No error state boilerplate. React handles it automatically with Suspense.

💡 Note: use() works with React 19+. Check your React version before using it.


Alternative 3: TanStack Query — For Serious Data Fetching

For production apps, TanStack Query (formerly React Query) is what senior devs reach for instead of useEffect fetching.

The useEffect way — full of problems:

// ❌ This has race conditions, no caching, no refetching
useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(setUser);
}, [userId]);
Enter fullscreen mode Exit fullscreen mode

The TanStack Query way:

// ✅ Caching, refetching, loading states — all handled
import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error loading user</p>;

  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

With TanStack Query you automatically get:

  • Caching — same data isn't fetched twice
  • Background refetching — data stays fresh
  • Loading & error states — built in
  • Race condition prevention — handled automatically
  • Pagination & infinite scroll — easy to implement

💡 Install: npm install @tanstack/react-query


Alternative 4: Direct Render Calculation — The Simplest Fix

This one is embarrassingly simple, and yet so many developers miss it.

If you're using useEffect just to compute something and store it in state — you don't need useEffect or useMemo. Just calculate it directly during render.

// ❌ Over-engineered with useEffect
const [totalPrice, setTotalPrice] = useState(0);

useEffect(() => {
  const total = cartItems.reduce((sum, item) => sum + item.price, 0);
  setTotalPrice(total);
}, [cartItems]);

// ❌ Still over-engineered with useMemo (for simple cases)
const totalPrice = useMemo(() => {
  return cartItems.reduce((sum, item) => sum + item.price, 0);
}, [cartItems]);

// ✅ Just... calculate it
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
Enter fullscreen mode Exit fullscreen mode

That's it. React recalculates it on every render — and that's completely fine for simple calculations.

Only use useMemo when:

  • The calculation is genuinely expensive (thousands of items)
  • You've profiled it and confirmed it's a performance bottleneck

For everything else, just calculate it inline.


Alternative 5: useLoaderData from React Router — For Route-Level Fetching

If you're using React Router v6.4+, you're probably still fetching data inside components with useEffect. Stop.

React Router has a built-in solution called loaders that fetches data before the component renders.

The old way — fetching inside component:

// ❌ Data fetches AFTER render — causes loading flicker
function UserPage() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <div>{user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The React Router way — loader + useLoaderData:

// ✅ Data fetches BEFORE render — no loading flicker
// Step 1: Define loader outside component
export async function loader({ params }) {
  const user = await fetch(`/api/users/${params.userId}`).then(r => r.json());
  return { user };
}

// Step 2: Use the data inside component
import { useLoaderData } from 'react-router-dom';

function UserPage() {
  const { user } = useLoaderData();
  return <div>{user.name}</div>; // Data is already here!
}

// Step 3: Attach loader to route
const router = createBrowserRouter([
  {
    path: '/users/:userId',
    element: <UserPage />,
    loader: loader,
  }
]);
Enter fullscreen mode Exit fullscreen mode

The component renders with data already available. No loading state needed. No useEffect. No flickering UI.


The Decision Framework — Which One Should You Use?

Next time you're about to write useEffect, ask yourself:

Am I computing something from existing state/props?
    → YES: Use useMemo or direct calculation
    → NO ↓

Am I fetching data for a route?
    → YES: Use useLoaderData (React Router)
    → NO ↓

Am I fetching data in a component?
    → YES: Use TanStack Query or React 19's use()
    → NO ↓

Am I syncing with an external system (DOM, WebSocket, third-party lib)?
    → YES: useEffect is correct here ✅
    → NO: Rethink your approach
Enter fullscreen mode Exit fullscreen mode

Quick Cheat Sheet

Situation Use This
Derived data from state/props useMemo or direct calculation
Simple data fetching (React 19) use() hook
Production data fetching TanStack Query
Route-level data fetching useLoaderData
External system sync useEffect

The Real Lesson

useEffect isn't bad. It's just misused.

It's a precision tool for syncing React with the outside world — not a general-purpose Swiss Army knife for every side effect imaginable.

When you start reaching for the right tool for each job, your React code becomes:

  • 🚀 Faster (fewer unnecessary renders)
  • 🧹 Cleaner (less boilerplate)
  • 🐛 More reliable (fewer subtle bugs)

Start small. Pick one useEffect in your current project and ask: "Does this actually need to be here?"

You might be surprised by the answer.


Which of these alternatives are you going to try first? Or do you have a useEffect horror story from your own codebase? Drop it in the comments — I'd love to hear it! 👇


Heads up: AI helped me write this.But the ideas, code review, and learning are all mine — AI just helped me communicate them better. I believe in being transparent about my process! 😊


Top comments (2)

Collapse
 
kenimo49 profile image
Ken Imoto

Great practical guide! I've suffered from useEffect state management and racing issues across many projects myself. The decision framework at the end is especially helpful having a clear mental model of "when to actually use useEffect" would have saved me hours of debugging stale closures and infinite loops. TanStack Query was a game-changer for us too. Once we switched, the amount of boilerplate we eliminated was shocking

Collapse
 
harsh2644 profile image
Harsh

Totally agree with you! 🙌

useEffect can quickly become messy if we don’t have a clear mental model. I’ve also wasted hours debugging stale closures and unexpected re-renders.

That’s exactly why tools like TanStack Query feel like a breath of fresh air — less boilerplate, fewer bugs, and way more focus on actual product logic instead of state juggling.

Glad this resonated with you!