DEV Community

Part 1: Why I Rarely Use useEffect Anymore (and what I use instead)

When I started learning React, I thought useEffect was the solution to almost everything. To derive state, to fetch data, to synchronize props or calculate a value, the answer for all this was useEffect. After working on larger applications, I realized most of those effects weren't needed at all.

Today, I probably write 80% fewer useEffects than I did a few years ago. Not because it's a bad hook, but because most problems have simpler solutions. This article is the first part of my Modern React Patterns series, where I go over patterns that worked fine in small demos but became painful in production.

What useEffect Is Actually For

The React documentation describes effects as a way to synchronize your component with external systems. That means things like:

  • network requests
  • WebSockets
  • timers
  • browser APIs
  • subscriptions
  • third-party libraries

If your effect isn't interacting with something outside React, there's a good chance you don't need one.

1. Don't Use useEffect for Derived State

One of the most common examples I still see is this:

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

It works, but React now renders twice. The process that is now doing is the following: render โ†’ effect runs โ†’ state changes โ†’ render again. And there's no reason for that.

Instead, I do this:

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

2. Don't Synchronize Props into State

const [user, setUser] = useState(props.user);
useEffect(() => {
  setUser(props.user);
}, [props.user]);

Most of the time this creates two sources of truth. Eventually one of them gets updated and the other doesn't. Unless you intentionally want a local editable copy, just use the prop directly. It's much simpler:

function Profile({ user }) {
  return <h2>{user.name}</h2>;
}

3. Don't Calculate Values Inside an Effect

I've seen code like this many times:

const [filteredUsers, setFilteredUsers] = useState([]);
useEffect(() => {
  setFilteredUsers(users.filter(user => user.active));
}, [users]);

That's unnecessary state. It's way better:

const filteredUsers = users.filter(user => user.active);

If the computation is actually expensive:

const filteredUsers = useMemo(() => {
  return users.filter(user => user.active);
}, [users]);

Notice that useMemo is an optimization, not a replacement for every calculation.

4. Debugging Is One of the Few Places Where I Still Use Effects a Lot

When debugging state updates I often do this:

useEffect(() => {
  console.log(user);
}, [user]);

It's incredibly useful while debugging, but before merging the code, delete it.

5. Fetching Data with useEffect Isn't Always the Best Choice Anymore

Years ago almost every React project looked like this:

useEffect(() => {
  fetch("/api/users")
    .then(res => res.json())
    .then(setUsers);
}, []);

Nowadays we have much better alternatives. Libraries like TanStack Query or SWR solve things that you eventually end up building yourself:

  • caching
  • retries
  • background refetching
  • loading state
  • error state
  • deduplication

And instead of all of this, you can:

const { data, isLoading } = useQuery({
  queryKey: ["users"],
  queryFn: getUsers
});

That's less code, and you get fewer bugs and a better developer experience. If you're interested in API reliability, I wrote another article about common integration mistakes: "Building Reliable API Integrations in Modern Web Applications".

6. Effects Often Hide Architecture Problems

Something I've noticed over time is that whenever I find myself writing lots of effects inside the same component, it's usually because the component is trying to do too much. For example:

  • fetching
  • filtering
  • sorting
  • formatting
  • validation
  • event handling
  • rendering

...and everything together. Splitting responsibilities into smaller hooks or components often removes half of those effects automatically.

When You SHOULD Use useEffect

None of this means "never use useEffect". There are plenty of legitimate use cases to use it, and let me tell some of them.

WebSocket connections:

useEffect(() => {
  const socket = new WebSocket(url);
  return () => socket.close();
}, []);

Timers:

useEffect(() => {
  const id = setInterval(fetchData, 5000);
  return () => clearInterval(id);
}, []);

Browser APIs:

useEffect(() => {
  window.addEventListener("resize", onResize);
  return () => {
    window.removeEventListener("resize", onResize);
  };
}, []);

Third-party libraries:

useEffect(() => {
  const chart = new Chart(canvas);
  return () => chart.destroy();
}, []);

These are exactly the kinds of external systems effects were designed for.

My Personal Rule

Whenever I'm about to write an effect, I stop for a second and ask myself: am I synchronizing with an external system, or am I compensating for my component design? That question alone has removed a surprising amount of unnecessary code from my projects.

Conclusion

useEffect isn't bad. It's just much easier to overuse than most React hooks. Today I try to use:

  • derived values instead of derived state
  • props instead of synchronized state
  • TanStack Query for server state
  • custom hooks for reusable logic
  • effects only when I actually need to synchronize with something outside React

And you can get fewer renders, less state, fewer bugs, and components that are easier to understand months later.

Next article in this series: Stop Fetching Data Inside useEffect (what modern React apps do instead)

Thanks for reading! If you're looking for help with an existing project or need someone to solve a tricky frontend/backend issue, you can also find my freelance profiles through my Dev.to profile.

Comments

No comments yet. Start the discussion.