How useEffect Works

In the newest version of React, you do not need to create a class component to maintain the state. There are a lot of articles and talks on why it is beneficial to get rid of the class component and use functional components. This discussion is beyond the scope of this post hence I would recommend if you want to know and understand the insight behind the decision of incorporating states and lifecycle (like) methods in functional components, you should go ahead and read/view them.

To keep things simple, I am not going to do a comparison between a class component and functional component, instead, I am going to talk only about functional components and how effects are used.

An effect (read side-effect) function is something that you want to run when you do not know the outcome or when you want to wait for the first render before running this function, for example, adding an event listener to the document, fetching data from the server, comparing the previous props received by the component with the new props.

Let’s take a small example.

function UpdateDomTitleAfterFirstRender() {
  /*
   * do something here, this will happen before
   * the component is rendered.
   */
  useEffect(() => {
    document.title = "From Use Effect";
  }, []);
  return (<div>Hello React!</div>);
}

To understand how useEffect works it is important to understand the sequence of execution:

  • As soon as this function starts execution, i.e. when this component is injected in the page, the first line of the function body will be (do something). Like any normal function.
  • Next in the sequence is the useEffect function. But if you see closely, useEffect function accepts two parameters:
    • A callback function, the function you want to execute.
    • An array (which is empty in this case, I will come to that later).
  • The component returns the JSX which is converted to HTML and injected to the DOM/VDOM.

The Callback

If you kept a close eye on this sequence, you must have seen that your function is passed as a callback and callbacks are not executed immediately. Check the following example.

// this is the typical setTimeout example that you must have come
// across at least once in your JS programming career.

console.log("A");
console.log("B");
setTimeout(function() {
  console.log("After 2 seconds printing C");
}, 2000);
console.log("D");

// output
A
B
D
After 2 seconds printing C    // after 2000 ms

The setTimeout waits at least for the given amount of time, it could go higher based on call stacks, but not less. The point is, setTimeout determines when to release your callback function in the execution.

Similar to that, useEffect also determines when your callback function will be executed. And that is: after the component is rendered.

The Array a.k.a The Dependency List

One of the key concepts of React is that it re-renders the component when the state of the component is updated. So wouldn’t you argue that if somehow we manage to update the state of the state using useState hooks inside the callback passed to the useEffectthere will be infinite re-rendered considering the callback is executed after the render considering:

  • state is being updated in the callback passed to useEffect which is executed post render.
  • updating state triggers a re-render.

Which means,

render => update state via useEffect => render => update state via useEffect

Try running the following code and with the debugger set inside the function,

let index = 0;

function InfiniteRender() {
const [tempVal, updateTempVal] = useState(0);
useEffect(() => {
updateTempVal(++index);
});
// notice the second argument has not been
// passed to useEffect.

return <div>Temp Val {tempVal} </div>;
}

See it in action: https://codesandbox.io/s/tender-shaw-rw15q

Note: If you are not aware of how useState works, read this post.

This usually is not the desired thing. A more real-life example would be:

  • We want to show some data by fetching it from a server.
  • After the response is received from the server, update the state with the received data, so that a re-render takes place to update your placeholders with fetched data.

So, how do we stop this infinite re-render?

The answer lies in the second argument. The array passed to the useEffect also known as the dependency list [list, thus an array]. Whatever we add to the array list, is given to useEffect. It checks and compares the previous value with the current value of the given variable in the list. If they do not change, useEffect cancels the effect and does not execute the callback function.

So if we pass an empty array, it means there is no change in the dependencies. Which in turn means, the effect will execute only once.

Go ahead and change the code and add an empty [] array as the second argument of the useEffect. Also, try to add tempVal to the depedency array and see what happens.

So if we pass an empty array list i.e. no dependencies, as the second argument to the useEffect, we can imitate the effect to behave like componentDidMount life-cycle method.

Key points:

  • The callback function passed to useEffect is executed after the component is rendered.
  • The useEffect accepts a dependency array as the second argument which it uses to compare the previous and current value. If only there is a change in any of the variables passed as the dependencies, the useEffect will execute the callback function.

So if you manage your dependencies, you will be able to imitate all the life-cycle methods. Because all life-cycle methods do is, they execute your code at different point of time. Let me know in comments if you need to understand any particular life-cycle method’s implementation using effects.

componentWillMount and componentWillUnmount

The componentWillMount is very simple. We just need to execute the code we want before the functional component returns the JSX, i.e. anywhere before the return statement.

The componentWillUnmount is a little different but it is simple. We need to return a function from the callback function we pass to the useEffect and it will be executed after the component will unmount. See example below.

.
.
.
useEffect(() => {
  document.addEventListener("click", someCallback);

  // this returned function will be executed
  // when the component will unmount/destroyed
  return () => {
    document.removeListener("click", someCallback);
  }
}, []);
.
.
.