DEV Community

Teguh Coding
Teguh Coding

Posted on

Stop Using useEffect for Everything: A Better Approach in Modern React

Stop Using useEffect for Everything: A Better Approach in Modern React

The Problem with useEffect Overuse

If you have been writing React for a while, you have likely experienced this: you need to fetch data when a component mounts, so you reach for useEffect. Then you need to update some state based on props changing, so you add another useEffect. Next thing you know, your component has five different useEffect hooks scattered throughout, and you have lost track of when each one runs.

I have been there. The useEffect hook feels like a superpower - it can handle side effects, data fetching, subscriptions, and more. But with great power comes great responsibility, and it is easy to abuse this hook.

When useEffect Actually Makes Sense

Let me be clear: useEffect is not bad. It is an essential tool. The problem is using it as the default solution for every problem.

Legitimate use cases for useEffect:

  • Subscribing to external data sources (WebSocket, EventSource)
  • Manually interacting with the DOM (when refs are not enough)
  • Logging analytics events
  • Cleanup operations that can not be handled otherwise

The Better Alternatives

1. Fetch Data Directly in Components (Server Components)

In Next.js App Router, you can fetch data directly in your component:

// app/users/page.jsx
async function UsersPage() {
  const users = await fetch("https://api.example.com/users", {
    cache: "no-store" // for dynamic data
  }).then(res => res.json());

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

No useEffect, no loading states to manage manually, no race conditions. Just pure, simple code.

2. Use use() for Promise Resolution

React 19 introduced the use() hook, which makes handling promises elegant:

import { use } from "react";

function UserProfile({ userPromise }) {
  const user = use(userPromise);

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

This is so much cleaner than the old:

const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  userPromise.then(data => {
    setUser(data);
    setLoading(false);
  });
}, [userPromise]);

if (loading) return <Spinner />;
Enter fullscreen mode Exit fullscreen mode

3. Derive State from Props

Instead of syncing state with props via useEffect:

// Do not do this
function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  useEffect(() => {
    setCount(initialCount);
  }, [initialCount]);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Do this instead
function Counter({ initialCount }) {
  // Just use the prop directly if it is the source of truth
  const [count, setCount] = useState(initialCount);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Or derive when you need to transform
function UserGreeting({ firstName, lastName }) {
  const fullName = `${firstName} ${lastName}`; // Derived, no state needed
  return <h1>Hello, {fullName}!</h1>;
}
Enter fullscreen mode Exit fullscreen mode

4. Handle Events Directly

Instead of using useEffect to respond to state changes:

// Do not do this
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState("");

  useEffect(() => {
    onSearch(query);
  }, [query, onSearch]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// Do this
function SearchInput({ onSearch }) {
  const [query, setQuery] = useState("");

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    onSearch(value);
  };

  return <input value={query} onChange={handleChange} />;
}
Enter fullscreen mode Exit fullscreen mode

A Mental Model Shift

The key insight is this: think in data flow, not in lifecycle events.

Ask yourself:

  • Where does this data come from? (Props, server, context)
  • Who owns this state? (Parent, local, server)
  • When should this update? (On event, on render, on mount)

Modern React (with Server Components, use(), and better patterns) encourages you to write code that describes what you want, not when to do it.

Conclusion

useEffect is not going anywhere, but it is not the first tool you should reach for anymore. With Next.js App Router, React Server Components, and hooks like use(), you can write simpler, more maintainable code.

Next time you are tempted to add a useEffect, pause and ask: "Is there a simpler way?"

Your future self will thank you when debugging at 2 AM.


What is your take? Have you moved away from useEffect in your projects? Let me know in the comments!

Top comments (0)