Skip to main content
Photo from unsplash: pramod-tiwari-Z9YaZTi27T8-unsplash_mku4xo

React Core Concept: useEffect

Written on January 12, 2021 by Kevin

5 min read

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'); });
jsx

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 /> ) }
jsx

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

useEffect(() => {}); useEffect(() => {}, []); useEffect(() => {}, [foo, bar]);
jsx

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

// run on initial render and every re-render useEffect(() => { console.log('hi'); });
jsx

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

// Run on initial render useEffect(() => { console.log('hi'); }, []); // When dependency changes, it will run useEffect(() => { console.log('hi'); }, [deps]);
jsx

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


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> ); }
jsx

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]);
jsx

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> ); }
jsx

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]);
jsx

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.