DEV Community

Cover image for useEffect - The Hook React Never Should Have Rendered
Dennis Persson
Dennis Persson

Posted on • Originally published at perssondennis.com

useEffect - The Hook React Never Should Have Rendered

React was once a fantastic library. Quick to learn, easy to customize and made it easy for you to write clean code. At least it used to be, at the time when they first white-painted the library with hooks instead of the old browny class components.

When React repainted the library, they did a mistake. They left a red brush in the white bucket which they now are using to paint the whole library into a pink disaster. React repainted their future with React 18, but in that picture, the useEffect hook doesn't belong.

I think it's time to get rid of that dangerous and disputed hook, but until then, I'll give you some tips of how to use it without getting pink paint all over your code.

In This Article

useEffect - The Red Brush in the White Bucket

React have added a wonderful useEffect hook to their bucket of hooks. Only problem, it doesn't belong there. When React 18 first came, the web exploded with warnings about useEffect, claiming it shouldn't be used and that we should not think about React in life cycles.

Truth is, useEffect was an issue long before that. It was very common to see developers leaving out the dependency array when they intended to have an empty dependency array, which causes the useEffect to be effectively useless.

And since React unfortunately leaves a lot of the optimization of the code to the developer, you may need to optimize your code using useCallback, otherwise you can get stuck in infinite rendering loops. Or, in a similar way, it can happen that your code will run on every render.

const ExampleComponent = () => {
  // This function will be triggered on every render.
  const getData = () => {
    // Get some data from server.
  }

  useEffect(() => {
    getData()
  }, [getData]);

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode
const ExampleComponent = () => {
  // Solution is to optimize the code with a useCallback...
  const getData = useCallback(() => {
    // Get some data from server.
  }, [])

  useEffect(() => {
    // We could also place the getData function in the useEffect,
    // with the drawback of readability if the function is big.
    // const getData = () => { ... }

    getData()
  }, [getData]);

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode

In earlier days, there were also a lot of articles about why you always should add all dependencies to your useEffect, even when you don't want the effect to run when some of the dependencies changes. That lead to ugly hacks with if statements to avoid code from running.

import { useEffect } from 'react'

const Component = ({ someOtherDependency }) => {
  const [value, setValue] = useState(null)

  useEffect(() => {
    // A common seen if statement added just to
    // prevent the effect from running.
    if (value === null) {
      setValue("someValue")
    }
  }, [value, someOtherDependency])

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode

Other alternatives were more clean, but only usable in a few cases. Below example shows a trick to avoid adding a state variable to the dependency array.

import { useEffect } from 'react'

const Component = () => {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // By passing a function to setCount, we can access its old
    // value without adding count to the dependency array.
    setCount(oldCount => oldCount + 1)
  }, [])

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode

React Keeps Painting With the Red Brush

If it wasn't difficult enough to make use of useEffect in React 17, React 18 made it even more troublesome. The main problem with the hook today isn't ugly if statements, missing dependencies or the fact that you should add all dependencies to the dependency array regardless of if you want to or not. Fact is, even if you do all of that, your useEffect may still run several times, due to concurrent rendering.

Luckily, React creators noticed concurrent mode caused a lot of concerns, so they solved it by only enabling it in parts of your code that uses the new React 18 features. A pretty good solution I would say. Pretty.

Problem is, the React docs for useEffect is quite extensive. When opting in for the new features, and thereby turning on concurrent mode, the docs for useEffect gets even more complicated. One page describing useEffect is not enough, you need another long page, and another one, and another, and another...

You have to think about if concurrent mode is enabled. You have to think of all the pitfalls. You have to think of all the optimizations. You have to think about what is best practice. You have to think about how the component renders even if you are encouraged not to think in terms of the old class life cycle methods such as componentDidMount and componentDidUpdate.

React is no longer a framework that has a low learning code. It's quick to start building with, but it takes a long time to learn how to write it correctly without introducing plenty of bugs.

Two architectures meme
Visual representation of React. Can you spot the useEffect?

The Proper Way To Use useEffect

React is getting a lot of new good features. Meanwhile, useEffect is becoming more and more dangerous. This article would become way too long if I continued describing issues with useEffect, just look at how big documentation there is for it. For that reason, I will give you one single tip for how to handle useEffect.

Let useEffect render.
Enter fullscreen mode Exit fullscreen mode

With letting it render, I don't mean you voluntarily should let it render on every rendering. What I mean is that you should not be afraid of letting it render too many times. Add all dependencies to its dependency array and let it do its work every time it runs.

What you shouldn't do, is to prevent the effect from running in certain use cases. The below code is awful and extremely bug prone.

const getDataFromBackend = () => {
  // Some code.
}

const ExampleComponent = ( {
  someVariable,
  anotherVariable,
  thirdVariable
}) => {
  useEffect(() => {
    if (someVariable === null) {
      if (anotherVariable > 10
        && (thirdVariable !== undefined || thirdVariable !== null)
        ) {
        getDataFromBackend()
      }
    }
  }, [someVariable, anotherVariable, thirdVariable]);

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode

The ugly code above should look like this instead:

const getDataFromBackend = () => {
  // Some code.
}

const ExampleComponent = ( {
  someVariable,
  anotherVariable,
  thirdVariable
}) => {
  useEffect(() => {
    getDataFromBackend()
  }, [someVariable, anotherVariable, thirdVariable]);

  return <div>Example<div>
}
Enter fullscreen mode Exit fullscreen mode

But? We cannot spam backend with plenty of network requests? Can we?

No, you should not spam backend. What you should do, is to make sure to write getDataFromBackend only fetch data when necessary. Not with the help of if statements, but by using caches, debounce or throttling.

Hooks like useSWR, useQuery and RTK Query handle such things for you, with some need for configurations. Using hooks like those are quite essential nowadays, not only because of issues with useEffect, but also because they include lots of logic you otherwise would have to implement yourself, with retries and state handling.

Importance of Idempotency

Under previous heading we could see how to properly fetch data in a useEffect. I also claimed that you should avoid preventing useEffect from running. In some cases, that might not feel possible. Sometimes we cannot use caches or debounce, that may be the case when sending POST requests to backend.

PUT requests should be fine, because they are idempotent by definition, meaning, it doesn't matter how many times an action is triggered, the result of calling it multiple times is the same as calling it a single time.

Sending POST request to a server are not necessarily idempotent, calling it multiple times may cause a unintended behavior or causing bugs. How can we handle that if useEffect can be triggered multiple times?

Answer is, try to avoid using a useEffect at all. There are multiple ways to do that, but it depends on the situation. If the function isn't dependent on the React component's state, it is possible to lift out the function out of the component.

Another example is when triggering effects on user interactions. In that case, you don't need an effect at all. I often see code similar to the code below.

import { useEffect } from 'react'
import { sendPostRequest } from '/services'

const Component = () => {
  const [buttonClicked, setButtonClicked] = useState(false)

  // Send a request when the button has been clicked.
  useEffect(() => {
    if (buttonClicked) {
      sendPostRequest()
    }
  }, [buttonClicked])

  return <button onClick={() => setButtonClicked(true))}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

The thing is, you don't need that useEffect, not even the useState. The code above should look like the code below.

import { useEffect } from 'react'
import { sendPostRequest } from '/services'

const Component = () => {
  // This is what you should do if you really want to send the
  // request when the button is clicked.
  return <button onClick={() => sendPostRequest())}>Click me</button>
}
Enter fullscreen mode Exit fullscreen mode

There are many other ways to get rid of useEffects, you can google how avoid useEffect. But sometimes you do need the effect, and in that case, you should make sure the code within it is idempotent, and then let the effect run as it wants to.

TL;DR

React is getting more and more difficult to learn and use. Optimizations are left up to developers and it gets more and more easy to write React code in a wrong way. React has very soon a steep learning curve and the useEffect is one of the main reasons to that.

To make the best out of the situation, one should try not to care too much about optimizing useEffects - let useEffect run many times. Then make sure the code within the effect is idempotent. For fetching data, you have really nice hooks like like useSWR, useQuery and RTK Query which helps you cache requests.

If a cache isn't the solution, a debounce or throttle may be. Many times you can even remove the useEffect completely since the code can be rewritten in a better way without it.

One day, we may see a React without the useEffect, and maybe even without useCallback? Or maybe the issues with the useEffect hook will keep on growing? In the end, I would say React is one hook away from being a wonderful framework.

Top comments (16)

Collapse
 
pcjmfranken profile image
Peter Franken

I really cannot find myself in the premise that the library doesn't take enough work out of its consumers hands, and that the best or even only viable solution is to use some third-part library that does.

The library leaving these things up to us makes it far more reliable and versatile than it would otherwise have been (in fact, I believe it still does too much obscure stuff behind the scenes). React's documentation is also very extensive and explains all these things quite thoroughly.

The problem I see is that many people didn't RTFM (Read The Fucking Manual), and the real solution wouldn't be to immediately direct everyone to a library, but rather to help them understand what's going on like you do in this post.

Collapse
 
perssondennis profile image
Dennis Persson

More people should definitely read the docs. Too often teammates asks "how can you know that" when the answer is "it's in the docs".

I would say it's really important for developer teams to actually get some time for learning the framework they are using.

Lots of developers starts a job without knowing the framework very well. And unfortunately, it takes lot of time to learn it because they only learn from their mistakes and not from the docs :)

Collapse
 
marcelorafaelfeil profile image
Marcelo Rafael

Your comment is absolutely correct. There are two different worlds when you learn the framework putting your hands on that and when you learn reading the documentation. I always learned a new technology putting my hands on the work and this attitude has its value, but the point is that the way is very important and you need to care about how the things work. I'm reading carefully the react documentation and I'm very impressed with the new vision that is being created on me about the framework.

Thanks for your article.

Collapse
 
nevodavid profile image
Nevo David

Thank you.

I almost never use useEffect with dependencies.

I doesn't matter if it's optimized or not, it leaves room for unnecessary rendering.

It reminds me the catastrophic beahvior on angular1 $watch.

Use a callback anywhere you can.

Collapse
 
urielbitton profile image
Uriel Bitton

If you're not putting variables in the dependency array, your useeffect is kind of useless. It probably won't even run the code inside it properly, since when react mounts, any fetched data hasn't loaded yet. So all your fetches will return empty or null. Good luck listening to this completely incorrect article though!

Collapse
 
nevodavid profile image
Nevo David

I am using useEffect to wait for the dom to finish loading (once) to do some logic and not as a variable watcher.

I am not sure what you are referring to here.

And Shalom :)

Collapse
 
leob profile image
leob • Edited

Quote:

"since React unfortunately leaves a lot of the optimization of the code to the developer ..."

For me that's the fundamental problem with React (even though, yes, I'm using React right now on a project, because it was the only viable choice).

Why should frontend devs be concerned with low level optimizations, which the framework can take care of? We should be able to focus on business logic, and UI/UX.

The root cause of the problems is React's programming model of "render always and figure out if something's changed" ... perfect in theory, not so much in the real world.

I'm convinced that frameworks based on Signals will have the edge, it's only React's huge market share that will delay this shift. Funny that "Signals" is now all the rage and a fancy buzzword, while a framework like Vue has had this for ages :)

Collapse
 
perssondennis profile image
Dennis Persson

You are right on yor points. There are frameworks very similar to React as well, such a s Solid.js, which already are a lot faster than React.

However. Faster and better is not enough. React have all these libraries and framework which are based on it, plus all of the developers already knowing it and have it labelel in their resume.

Those things are difficult to beat. It's not easy moving over to another new library where you have to develop a lof ot the things you are used to have from scratch.

Collapse
 
leob profile image
leob • Edited

Yes, spot on - critical mass, market share, ecosystem, community - all of those make React the 800 pound gorilla (I had to look that up, lol, wasn't sure how many pounds that gorilla was), and "the safe and default choice" in many cases - a bit like, "nobody ever got fired for choosing IBM (or Microsoft)".

P.S. my "use case" was that I was planning to create a mobile frontend, and we had settled on Ionic (which in itself was already a compromise), and I refused to learn Angular (the "default choice" for Ionic) from scratch - so I decided to go with Ionic's React integration ... which brought a host of other "surprises", as Ionic's routing model and "life cycle" turned out to be rather "different" from React's default :)

Collapse
 
brense profile image
Rense Bakker

I don't fully agree with your conclusion, but the explanation and tips are sound πŸ‘

Collapse
 
urielbitton profile image
Uriel Bitton

React is great, it has no problems if you know and actually understand how to use it correctly. If you don't like it just use another framework.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

Yes, articles like this do not help anyone. Svelte is so far behind if you only knew the details you would learn useEffect.

Collapse
 
urielbitton profile image
Uriel Bitton

Exactly.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.