Skip to main content
Photo from unsplash: genessa-panainte-sBvK15KlpYk-unsplash_v5yhfy

React Core Concept II: useEffect

Written on November 02, 2022 by BJUT Swift.

6 min read
––– views

Introduction

The useEffect hook is something that is quite hard to grasp for me at first, but it turns out it is not that complicated. With this post, I'm going to introduce you to a simple mental model that might help you to understand the basic concept.

Quick Recap

Before you continue to read this post, it is best to read my first React Core Concept article about useState because I'm going to reference some mental models used in the last post.

In the last post, this is something that you need to remember:

React does a re-render by calling the component function.

React will trigger the render function when

  1. The useState value changes (using setState)
  2. The parent component re-renders
  3. The props that are being passed changes

Looking at useEffect

If you used useEffect before, you must've known that it would run the arrow function inside the useEffect. It is written like this.

React.useEffect(() => {
  console.log('hello');
});

When we see the structure of the useEffect hook, it resembles a cloak that wraps one function. Now, we need to know what that cloak does to our function.

Controlling Functions with useEffect

With useEffect, we can control when would we like to run the function.

Let's see an example:

export default function Test() {
  fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then((res) => res.json())
    .then((data) => console.log(data);
 
	return (
		<Component />
	)
}

Do you notice what's wrong with the example? Yes. The fetch will be run every single re-render, and we probably don't want that.

We can fix that problem by controlling when should the function run using useEffect. We can control it with the deps parameter


Types of Dependencies

Here are the usual types of dependencies that are often used with useEffect

types-of-dependency

Before we break it one by one, there is a mental model that you need to remember.

The useEffect hook will always run once on the initial render. There is no exception.

Without Dependency

without-deps

Without the dependency parameter, it is practically the same as calling a function on the top level. The useEffect will run on the initial render and every re-render

There is a slight difference in using useEffect, I'll cover this at the end of the article because it is insignificant for now.

With Dependency

with-deps

When we introduce the dependency parameter, the mental model becomes like this:

When the dependency changes, I'll run

They will run on the initial render and whenever specified dependencies changed. We can specify the dependencies inside the array.

specified-deps

Emphasize the OR. So when we put foo into the array, the useEffect will run every time the foo variable changes.

Specifying Empty Array

empty-deps

When you specifically put an empty array to the dependency, it is like saying “When nothing changes, I'll run”, and we can paraphrase it to this mental model

I will never run on any changes

However, it will always run on the initial render, so using an empty array will cause the useEffect to run once on the initial render.

This is super useful when you need to fetch data from another API. You'll run it only at the initial render and show it to the user.

Difference between Empty Parameter and Empty Array

This is something that you also need to note:

Not giving any parameter is different than specifying an empty array

empty-parameter-vs-array

How does React decide if the dependency changes?

React is going to compare them using shallow comparison. Here are some cases

1. Primitive dependency

Primitive including boolean, string, numbers, etc.

export default function TogglePage() {
  const [toggle, setToggle] = React.useState(false);
 
  console.log('🔥 Rerender');
 
  React.useEffect(() => {
    console.log('🔵 Effect');
  }, [toggle]);
 
  return (
    <div>
      <Button onClick={() => setToggle((t) => !t)}>Toggle</Button>
    </div>
  );
}

Here's a really simple example, if you follow the tutorials correctly you're now able to infer that every time the button is clicked, it will log the Re-render and Effect log.

primitive-example

Easy right? The useEffect will run if it sees that the toggle value changes from true to false or vice versa.

primitive-deps

2. Object dependency

Before we jump into the example, I want to clarify this first.

const obj = {
  toggle: false,
};
 
React.useEffect(() => {
  console.log('🔵 Effect');
}, [obj.toggle]);

If you're assigning the object's property, it's going to follow that property value. So in this example, it's going to follow the primitive dependency mental model.

Let's get to the real example

export default function ChangePage() {
  const [toggle, setToggle] = React.useState(false);
  const [falseToggle, setFalseToggle] = React.useState(false);
 
  console.log('🔥 Rerender');
 
  const obj = {
    toggle,
  };
 
  React.useEffect(() => {
    console.log('🔵 Effect');
  }, [obj]);
 
  return (
    <div>
      {/* Clicking button will change falseToggle value */}
      <Button onClick={() => setFalseToggle((t) => !t)}>Toggle</Button>
    </div>
  );
}

Following the last mental model, you might conclude that the Effect log won't run because the toggle value doesn't even change right??

The answer is it will run the effect function.

That behavior is because, in every re-render, we're creating a new object. React is going to treat the object as a different value even though it is identical.

object-deps

If you're using ESLint, they actually will warn you to fix it using useMemo hook

eslint-memo
const obj = React.useMemo(() => {
  return { toggle: toggle };
}, [toggle]);

useMemo will use the existing object if the dependency doesn't change. Thus not creating a brand new object each time.

Notice something similar in the useMemo hook? Yes! it follows the same mental model for dependency. Learn something once, and you can use it for more than one concept.

Conclusion

The useEffect hooks work with 2 types of dependencies:

  • Without Dependencies
  • With Dependencies
    • Empty Array
    • Specified Array

When using specified dependencies it will compare them using shallow comparison with 2 mental models that you can remember which are primitive and object dependency.

Tweet this article

Enjoying this post?

Don't miss out 😉. Get an email whenever I post, no spam.

Subscribe Now