DEV Community

Cover image for React Hooks: useEffect
Hajar | هاجر
Hajar | هاجر

Posted on • Edited on

React Hooks: useEffect

Before talking about the useEffect Hook, let me ask you: Have you ever tried to do something like:

 document.getElementById("someId").classList.add("someClass");
Enter fullscreen mode Exit fullscreen mode

and then found out that React was now complaining TypeError: Cannot read property 'classList' of null?

Well, I most certainly have.

At first, I would be like, "What’s going on now? I have a div element with someId just right there!!!" And then I would realize, "Aha, React has not finished positioning all my HTML elements on the page yet. It does not know about the div with someId yet. So my div is null to React."

Now, let's talk about useEffect().

When I think of useEffect(), I imagine it as the first door that React knocks on, the moment it is done with positioning all HTML elements on the page. And therefore, it is where we should put the code that we want to run right after that happens so that React never complains about missing elements.

Since React renders every time a change happens to a component, it knocks on/calls useEffect() after every render.

The simplest structure of useEffect looks like this:

useEffect( ()=>{
     // code to run after every render/re-render
});

Enter fullscreen mode Exit fullscreen mode

Let's see a simple example.

import React {useState, useEffect} from 'react';

const App =()=> {
  let [circleBackgroundColor, setCircleBackgroundColor] = useState("lightblue");

  useEffect(()=>{
       // change the bg-color of the body after every render
       document.body.style.backgroundColor = getRandomColor();
    })

    const changeCircleBackgroundColor = ()=>{
      // change the value of circleBackgroundColor
      setCircleBackgroundColor(getRandomColor())
    }

    return(
      <main>
          <div 
            style={{backgroundColor:circleBackgroundColor}} 
            className="circle"
          />
          <button onClick={changeCircleBackgroundColor}>Change Color</button>
      </main>
    )
}

const getRandomColor = ()=>{
   return "#" + Math.random().toString(16).slice(2,8);
}
Enter fullscreen mode Exit fullscreen mode

Notice here that the bg-color of body changes not only on refreshing the page but also on changing the background color of the circle div.

Why does that happen?

On refreshing the page, the bg-color of body changes because React calls useEffect right after the first render.

On changing the bg-color of the circle div, the bg-color of body changes because when changing the state variable circleBackgroundColor, React has to re-render App. And after re-rendering, it calls useEffect again.

But wait, is that even efficient?
I mean, calling useEffect every time a tiny, little change happens to a component, is that efficient?

No, it is not. And most of the time, we would not want that to happen.

useEffect takes a second optional parameter called the dependency array. We pass the dependency array to useEffect to tell React not to bother calling useEffect every time a change happens to a component. To tell React that it just needs to keep an eye on the dependency array variables and that it needs to call useEffect only when any of these variables change.

So that's how useEffect should look like now:

useEffect(()=>{
     // code to run only on the first render and when depency1 or/and depency2 changes.
}, 
// the dependency array
[depency1, depency2]);
Enter fullscreen mode Exit fullscreen mode

Let's make a few changes to our last example and pass a dependency array to useEffect.

...
// onOff will be a dependency to useEffect
let [onOff, setOnOff] = React.useState(true);
...
useEffect(()=>{
       document.body.style.backgroundColor = getRandomColor();
    // add onOff in the dependency array
    }, [onOff])
}
...
 // just a button to toggle the value of onOff
 <button onClick={()=>setOnOff(!onOff)}>toggle onOff</button>
Enter fullscreen mode Exit fullscreen mode

If a component has useEffect, the first thing React does after the first render is to call useEffect -- whether it has a dependency array or not. After that, React gets to know if useEffect has a dependency array or not. And based on that knowledge React has to, well, react.

If there is not a dependency array passed to useEffect, React has to remember to call useEffect after all future renders.

If there is a dependency array passed to useEffect, React has to keep an eye on all these dependencies. And after any future changes happen to any of them, React will have to call useEffect.

Let's see that in code.

In this example, when React calls useEffect after the first render, it knows that there is an onOff dependency passed to it. So, it will then remember that it will not have to call useEffect unless onOff changes.

Hence, the bg-color of body changes only on the first render and every time onOff changes.

Notice that changing the bg-color of the circle div does not affect body anymore. React no longer calls useEffect after changing circleBackgroundColor.

So now, we have limited the calls to useEffect from "after every render/re-render" to "only after the first render and all the re-renders caused by the changes of the dependencies."

What if we want to limit the calls to even less than that?
What if we want to run useEffect only once, after the first render?

We can achieve that by passing an empty array as the dependency array to useEffect. By doing that, React will call useEffect after the first render and then forget all about it. It will never call it again.

Note: You have to be careful when using [] as the dependency array. Make sure that useEffect does not contain any variables (state or props) or functions from inside its component. Refactor and strip useEffect of all the variables and functions from inside its component before using [] as the dependency array, or you have to list them as dependencies.

One last thing to mention on this subject is that we can return a function at the end of useEffect. That function runs after every re-render (just right before the new effect runs), to clean up after the effect from the last render. It also runs right before the component is removed from the DOM.

If we change a little bit in the code form the earlier example:

    useEffect(()=>{
      // add a delay to see the effect of the clean-up function
      setTimeout(()=>{  
       document.body.style.backgroundColor = getRandomColor()
       }
      , 500);
       // the clean up function
       return ()=>{
          // it cleans up the bg-color of the body 
          // just right before the new effect tasks a place
          // and changes it again
          document.body.style.backgroundColor = "white";
       }
    }, [onOff])
Enter fullscreen mode Exit fullscreen mode

Here, when we toggle onOff, the cleanup function runs to clean up the bg-color of body before the new effect runs and changes it again.

That's it.
Thank you for reading.

Top comments (2)

Collapse
 
jhavn profile image
jhavn

I do wonder why you're no longer adding a class to an element in your examples. I might be wrong, but your examples don't answer the question in your first paragraph: how do you add a class (via classList.add) to an element that hasn't yet been found by React?

Collapse
 
hajarnasr profile image
Hajar | هاجر • Edited

Yeah, I didn't use an example to answer that question (sorry). However, I think what I wanted to say was that if we wanted to add some code that depended on the component's render, useEffect would be the right place to add that code. Because it gets called after all the elements are painted on the page. :)