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
- The useState value changes (using setState)
- The parent component re-renders
- 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
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 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
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.
Emphasize the OR. So when we put foo
into the array, the useEffect will run every time the foo
variable changes.
Specifying Empty Array
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
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.
Easy right? The useEffect will run if it sees that the toggle value changes from true
to false
or vice versa.
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.
If you're using ESLint, they actually will warn you to fix it using useMemo
hook
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.