UseEffect, What Does It Do?

Table of Contents

The basics

I'll start by saying I think useEffect is a stupid name.

Long story short, useEffect is a way to tell React "After you've finished updating the DOM, run this extra bit of code."

Here's how it should be called:

useEffect(setup, dependencies?)
But what does that even mean?

The setup argument

 

setup is the logic function of the "effect".

When the component mounts, or the dependency array changes (we'll get to that next), React runs the setup function. Here's an example:
const [count, setCount] = useState(0);

useEffect(() => {
    document.title = `You clicked ${count} times`;
}, [count]);
The dependency array argument

In the example above, we called useEffect with [count] as the dependencies argument. This means that our useEffect function is "listening" for the count state variable to change. It will call the setup function whenever count changes.

Also, as a bonus, it will also run whenever the component mounts.

Go ahead, try it, I dare you. Reload the page. Notice how the page title is already Count: 0 before you've even pressed the button?

What if I don't pass a dependency array?

Good question. And here's a good answer.

If you don't pass a dependency array to useEffect, the setup function will be called on every single re-render.
...so (except in rare circumstances), it's probably best to pass a dependency array. Or not. I don't care really.

What if I pass an empty dependency array?

Another good question.

If you pass an empty dependency array to useEffect, you're telling React:
"Run this code exactly once, and then never touch it again.".

This is useful for things like API calls, when you only want to fetch data from the server once when the page loads.

WTF is a cleanup function?

This is actually quite easy to explain. If you return a function from the setup function, congratulations, that's a cleanup function.

useEffect(() => {
    document.title = `Count: ${count}`;

    // Here's your cleanup function
    return () => {
        document.title = "Count cleared";
    }
}, [count]);

When is the cleanup function called? Whenever the component unmounts.

Think of it as turning the TV off and locking the doors before you go out. The cleanup function is used to tidy up your mess before the component unmounts. It's useful for things like:

  • Ending intervals/timers
  • Disconnecting from websockets
  • Etc. (I ran out of ideas)

A common use-case is for tearing-down things like event listeners:

useEffect(() => {
const handler = () => {
...
};

window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);

That way, we clean up event listeners each time the dependencies change.

With Typescript

Great news! You don't need to explicitly type this, useEffect also uses type inference.

Here's an example:

useEffect(() => {
    document.title = `Count: ${count}`;
}, [count]);
In this case, typescript will infer that the dependency array will be an array which matches the type of count, which is a number.

The setup function, should be exactly that... a function. Not an async function. Just a regular old function.

If you need to do async stuff inside the useEffect setup function, it should be wrapped inside a regular, synchronous function, like this:

useEffect(() => {
    const performAsyncTask = async () => {
        // async logic here
    };
    performAsyncTask();
}, []);

Not like this:

useEffect(async () => {
    const data = await fetchData();
}, []);

Because you'll get the following error:

    Argument of type '() => Promise<void>' is not assignable to parameter of type 'EffectCallback'.
    Type 'Promise<void>' is not assignable to type 'void | Destructor'.