How does useEffect() work?

Newton School Tech
Newton School
Published in
4 min readOct 12, 2021

useEffect hook is the solution to many problems: how to fetch data when a component mounts, how to run code when state changes or when a prop changes, how to set up timers or intervals, you name it.

All this power comes with a tradeoff: useEffect can be confusing until you understand how it works.

There are tons of posts/articles explaining the concept. In this post, we are going to learn the working of useEffect in the simplest terms and with examples (Nothing different from what we have been teaching students at Newton School)

useEffect() is for side-effects

A functional React component uses props and/or state to calculate the output. If the functional component makes calculations that don’t target the output value, then these calculations are named side-effects.

Examples of side-effects are fetch requests, manipulating DOM directly, using timer functions like setTimeout(), and more.

The working of useEffect

Let’s start with an example

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

useEffect(() => {
let timerId;

console.log("useEffect as mount");

const callback = () => {
setCount((c) => c + 1);
};

timerId = setInterval(callback, 1000);

return () => {
console.log("useEffect as unmount");
clearInterval(timerId, callback);
};
});

return <h1>{count}</h1>;
};

This code basically increases the count after each second and updates the DOM according to the current value of count.

Now, take some time and think what would be printed in console for this piece of code?
. . . . .

Surprising? Not if you completely understand how useEffect works.

Let’s Dive Deeper

Let us understand how useEffect works under the hood. We are going to divide the explanation of working into two parts

  1. Rendering
  2. Re-rendering

The Rendering Phase

  1. JSX is rendered.
  2. Body of useEffect is executed.
  3. cleanup function is stored in memory.

The Re-rendering Phase

  1. JSX is rendered.
  2. Stored cleanup function (if any) is executed.
  3. Body of useEffect is executed.
  4. cleanup function is stored in memory.

Interesting. Let’s now decode the Counter example used to demonstrate the hook.

What happens in the rendering phase?

During the render phase, counter starts with a value 0. Now an interval is set which increments the counter each second. It also prints useEffect as mount in the console. Finally, interval clearance function (acting as cleanup function here) is stored in memory.

What happens when it re-renders?

As soon as count variable is updated, useEffect executes again. Why?

This is because we have not passed the dependency array. In this case, useEffect runs on every re-render.

During the re-rendering phase, updated counter value is rendered in DOM. As per the execution steps, stored cleanup function now needs to be executed. That is, clearInterval function will be invoked and it will clear out the timer that started in the previous cycle. The execution continues and a new interval is set and clearance function is stored once again in the memory.

Pheww!! It’s a lot to digest. One might think that a continuous timer is updating the count variable each second but when decoded it is inferred that it's a new timer which is started after each render in order to increment the counter.

Run useEffect() only once

There might be times where you want to run the effect only once. To achieve the same, we need to tell React that when should it fire the effect. We can do it using the **dependency array**.

The dependency array in useEffect lets you specify the conditions to trigger it. If you provide useEffect an empty dependency array, it’ll run exactly once, as in this example

Adding variables to the dependency array tells useEffect: “Hey, I need you to run only if this value changes”. Interestingly, you can pass any kind of variable as a dependency in the dependency list.

It now becomes significantly easy to understand why does empty dependency array corresponds to componentDidMount in class components.

Food for thought

Using all the facts stated in the post, one might be able to deduce why cleanup function acts same as componentWillUnmount

const Counter = () => {  const [count, setCount] = useState(0);  useEffect(() => {     let timerId;     console.log("useEffect as mount");     const callback = () => {     setCount((c) => c + 1);
};
timerId = setInterval(callback, 1000); return function cleanup() { console.log("useEffect as unmount"); clearInterval(timerId, callback); }; }); return <h1>{count}</h1>;};

Conclusion

In our opinion, understanding the underlying design concepts and best practices of the useEffect hook is a key skill to master if you wish to become a proficient React developer.

--

--