DEV Community

Shivam pandey
Shivam pandey

Posted on

UseEffect in React18

In this article we are going to discuss why on earth useEffect is running twice in React18. Is it breaking your code or can it break your code and what to do if it breaks your code. How is it helping, if it is and what is the way ahead.

So you might have heard of it already as it is hottest gossip these days in the react universe that useEffects will now run twice in Strict mode while development. To be very specific react will simulate unmounting and remounting, i.e. previously what looked like this

* React mounts the component.
  * Layout effects are created.
  * Effects are created.
Enter fullscreen mode Exit fullscreen mode

will now look like this.

* React mounts the component.
  * Layout effects are created.
  * Effects are created.
* React simulates unmounting the component.
  * Layout effects are destroyed.
  * Effects are destroyed.
* React simulates mounting the component with the previous state.
  * Layout effects are created.
  * Effects are created.
Enter fullscreen mode Exit fullscreen mode

Hearing this most of us start rolling our eyes as it is not at all intuitive to anybody who is familiar to react. It literally got so controversial that Dan tweeted this at a point.

So lets see some piece of code which broke due to this behavior of useEffect.

Cation: The code you are about to see is just for demonstration purpose only, motive here is to write simple explanatory piece of code that points to the main discussion.

import "./styles.css";
import { useState, useEffect } from "react";

export default function App() {
  const [stopWatch, setStopwatch] = useState(30);
  const [intervalId, setIntervalId] = useState(0);

  useEffect(() => {
    let id = setInterval(() => setStopwatch((p) => p - 1), 1000);
    setIntervalId(id);
  }, []);

  useEffect(() => {


    if (stopWatch === 0) {
      clearInterval(intervalId);
    }
  }, [stopWatch]);
  return <div className="App">{stopWatch}</div>;
}

Enter fullscreen mode Exit fullscreen mode

so this is just a simple snippet which is trying to start a reverse counter and count till 0. In react 17 it would have worked just fine but check it out here this counter won't stop.

So lets try to determine what went wrong. I'll break it into steps

  1. useEffect ran and registered an interval that will update the state every second.
  2. Component simulated unmounting.
  3. useEffect ran and registered one more an interval that will update the state every second.
  4. Now 2 interval are updating count at the same time.
  5. When interval is cleared only one of them is cleared which is why it keeps going on and on.

By now you might have already figured it out this is a typical case of memory leak and we can easily fix it using a cleanup function. Lets hop to the code

import "./styles.css";
import { useState, useEffect } from "react";

export default function App() {
  const [stopWatch, setStopwatch] = useState(30);
  const [intervalId, setIntervalId] = useState(0);

  useEffect(() => {
    let id = setInterval(() => setStopwatch((p: number) => p - 1), 1000);
    setIntervalId(id);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  useEffect(() => {
    if (stopWatch === 0) {
      clearInterval(intervalId);
    }
  }, [stopWatch]);
  return <div className="App">{stopWatch}</div>;
}
Enter fullscreen mode Exit fullscreen mode

you can try to run this code here

let's again break it down what all happened:

  1. useEffect ran and registered an interval that will update the state every second.
  2. Component simulated unmounting and cleared the created interval.
  3. useEffect ran and registered one more an interval that will update the state every second.
  4. Now this interval will work as it should and we see no issues.

Did you see that? Running it twice is actually helping us to find potential memory leaks which we may miss otherwise and currently this is only in development and it won't happen in production anyways. So I don't think it's a bad deal at all.

So is that it?

Is it running twice to point memory leaks or is there some thing else as well?

Well React 18 has interesting things to be rolled out in future and this feature of simulating remounting of an component is just a preparation for the same. Let's look into it a little

Say you switch back and forth between two components. Some what like:

  1. Opens up component A and do something.
  2. Switch to component B
  3. Switch back to component A

Now you would have lost your state in A (yes, there can be way to manually cache it but lets only talk about potential of plain react without any trickery.) i.e. if you were filling a form or working on some thing your progress is lost. If that page makes some slow network request to render data it will again take time.

Now this problem can be resolved by caching DOM tree in memory and when it is remounted using the cached tree again to render as quick as possible. This is already implemented in frameworks like Vue. There, they call it keepAlive can read more about it here. I emphasize go on to this link and check the demo how it works so you get a better idea of it.

Now there is one caveat here. According to user they have reopened the component and so they should see some actions which should happen at remounting and here react team plans to simulate remounting.

Now if you think about it. It makes a lot of sense right? But then why to run it twice on development mode, simply to have the sanity before you run into something weird.

Just to mention in a conversation Dan mentioned that this will be an opt-in feature i.e. you can opt into it it will not happen but default.

Beware

With all the heated up conversations going around one particular way of solving the issue of useEffect got a bit fame but is actually a problem. Let's see what it is and why I say is a bit problematic.

We could have solved the problem discussed above in this way as well.

import "./styles.css";
import { useState, useEffect, useRef } from "react";

export default function App() {
  const [stopWatch, setStopwatch] = useState(30);
  const [intervalId, setIntervalId] = useState(0);
  const isInitialRender = useRef(true);

  useEffect(() => {
    let id;
    if (isInitialRender.current === true) {
      isInitialRender.current = false;
      id = setInterval(() => setStopwatch((p) => p - 1), 1000);
      setIntervalId(id);
    }
  }, []);

  useEffect(() => {
    if (stopWatch === 0) {
      clearInterval(intervalId);
    }
  }, [stopWatch]);
  return <div className="App">{stopWatch}</div>;
Enter fullscreen mode Exit fullscreen mode

You can try running this code here

Did it solved the problem? Yes, but did it actually solved the problem? No.

I see this code as problem in multiple dimensions, let's discuss about it.

  1. First things first using a hook where it is not needed. Any case can be written or re-written in a way that it will produce the right effect and if that is possible then there is no need to do this.

  2. This simulation of remounting of component is not there in production, so we definitely do not need any added over head to run in production.

  3. Why to swim against the flow? Just Imagine there would have been debates conducted for weeks before adding this feature of simulating remounting, they would have created POCs and what not. After a lot of effort they come up with a feature which is forcing devs to write good quality code and then someone turns that to void by using such techniques. Its better to test code and write it in cleaner way then to wait for a bug and then resolve it (no one really wants a SEV 1 ticket on a Friday night right). Sorry if I sounded like ranting a bit.

Conclusion

Crux of the story is simulating remounting in development mode is actually helping in ways. If some logic is breaking due to that, there must be definitely a different or better way to achieve the same functionality. Last but not the least, React has much more exciting features to come in near future.

Are you still left with some questions? Post them in comment below I'll try to help you out. Thank you for being such a patient reader and see you next time.

Discussion (1)

Collapse
sjtgshivam profile image
Shivam pandey Author • Edited on

Adding more to it. Components were always rendered twice(in some cases) in previous versions of Strict mode as well can check this thread github.com/facebook/react/issues/1... . So whats the difference now?

When a component is rendered the states that are created in previous render are destroyed and new references to newly created state variables are used in the second render.

While when simulating a re-render after simulating no new state is created and older state is used so if we made some changes to state in useEffect those changes will reflect and will be used as initial state of "simulated re-render". This is the main difference now.

Please do not confuse between two different things.