Understanding the flow of React's useEffect hook
Tags: javascript, react
Published: 2021-08-19 10:35:17

React's `useEffect` hook is used quite often in applications. It is used to perform side effects in your components like subscribing/unsubscribing to events, making API requests etc.

In this article we're going to discuss the flow in which things happen when working with this hook.

## Order in which "Setup" and "Cleanup" functions are called

The `useEffect` hook accepts a function as the only argument which is often referred as the "Setup" function and you can optionally return a function from this "Setup" which is often referred as the "Cleanup" function.

In this example we'll see the flow in which these Setup and Cleanup functions are called.

```jsx
const { useState, useEffect } = React;

const Counter = () => {
	const [count1, setCount1] = useState(0);
	const [count2, setCount2] = useState(0);

	useEffect(() => {
		console.log("useEffect no dependency ran");

		return () => console.log("useEffect no dependency cleanup ran");
	});

	useEffect(() => {
		console.log("useEffect empty dependency ran");

		return () => console.log("useEffect empty dependency cleanup ran");
	}, []);

	useEffect(() => {
		console.log("useEffect count1 as dependency ran");

		return () => console.log("useEffect count1 as dependency cleanup ran");
	}, [count1]);

	useEffect(() => {
		console.log("useEffect count2 as dependency ran");

		return () => console.log("useEffect count2 as dependency cleanup ran");
	}, [count2]);

	return (
		<>
			<button onClick={() => setCount1((c) => c + 1)}>{count1}</button>
			<button onClick={() => setCount2((c) => c + 1)}>{count2}</button>
		</>
	);
};

const App = () => {
	const [showCounter, setShowCounter] = useState(false);

	return (
		<main className="App">
			<label htmlFor="toggleCounter">Toggle Counter: </label>
			<input
				id="toggleCounter"
				type="checkbox"
				checked={showCounter}
				onChange={({ target }) => setShowCounter(target.checked)}
			/>
			<div>{showCounter && <Counter />}</div>
		</main>
	);
};

const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
```

Take a moment to understand the example above, it looks lengthy because it has a bunch of `useEffect` calls but its fairly simple otherwise.

Our focus is on the `Counter` component and all our logs are from this component. 

So, initially there are no logs because the `Counter` component is not yet mounted (_as `showCounter` state is set to `false`_).

<hr> 

### Let's click on the "Toggle Counter" checkbox

This updates the `showCounter` state and a re-render happens and we have our `Counter` mounted for the first time.

> **Logs**
> _useEffect no dependency ran
useEffect empty dependency ran
useEffect count1 as dependency ran
useEffect count2 as dependency ran_

💡 Observation: Notice all setups ran and they ran in the order they were called.

🚀 **This is because all Setups run on mount irrespective of the dependency array and they run in the exact same order in which we called them. Also, no Cleanups run on mount.**

(Clear the logs before moving to the next section)

<hr>

### Let's click on the first counter button

> **Logs**
> _useEffect no dependency cleanup ran
useEffect count1 as dependency cleanup ran
useEffect no dependency ran
useEffect count1 as dependency ran_

💡 Observation: Notice only two effects ran this time and both cleanup and setup ran for these two (_and they still run in order they were called_).

🚀 **Its because on re-renders the Effect hook (_both Cleanup and Setup_) runs only if the dependencies change (_`count1` changed_) or if the second argument is skipped completely.**

💡 Observation: Notice Cleanups run before Setups for both "no dependency" Effect hook and "count1" Effect hook.

🚀 **So, when both Cleanup and Setup has to run for a particular effect hook, the Cleanup will run before the Setup.**

If you want to explore why `useEffect` runs after every render and not just on unmount, [React docs](https://reactjs.org/docs/hooks-effect.html#explanation-why-effects-run-on-each-update) does a really great job of explaining this.

(Clear the console before moving to the next section)

<hr>

### Let's now click on the "Toggle Counter" checkbox again

This updates the `showCounter` state and unmounts the `Counter` component.

> **Logs**

> _useEffect no dependency cleanup ran
useEffect empty dependency cleanup ran
useEffect count1 as dependency cleanup ran
useEffect count2 as dependency cleanup ran_

💡 Observation: Notice all cleanups ran and they ran in the order they were called.

🚀 **This is because all Cleanups run on unmount irrespective of the dependency array and they run in order. Also, no Setups run on unmount.**

<hr>

🔥 CheatSheet

|Phase|Setups|Cleaups|Condition
|----|----|----|----|
|Mount|All|None|None|
|Re-render|Some|Some|Dependency Array|
|Unmount|None|All|None|

<hr >

## Children's useEffect hooks run before Parent's

Consider the example below, it's to explain a small point that Children's useEffect hooks will always run before Parent's useEffect hook.

```jsx
const { useEffect } = React;

const Child = () => {
	useEffect(() => {
		console.log("Child useEffect ran");
	});
	return <p>Child</p>;
};

const App = () => {
	useEffect(() => {
		console.log("App useEffect ran");
	});
	return <Child />;
};

const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
```

> **Logs**
> _Child useEffect ran
App useEffect ran_

<hr>

## `useEffect` hooks are called Asynchronously

The example below demonstrates a really important point which is that `useEffect` hooks are called Asynchronously.

```jsx
const { useEffect } = React;

const App = () => {
	console.log("Before useEffect");
	useEffect(() => {
		console.log("Inside useEffect");
	});
	console.log("After useEffect");
	return <h1>Hello World</h1>;
};

const rootEl = document.getElementById("root");
ReactDOM.render(<App />, rootEl);
```

> **Logs**
> _Before useEffect
After useEffect
Inside useEffect_

💡 Observation: Notice the "Inside useEffect" log is printed after the "After useEffect" log.

🚀 **And this is because React calls `useEffect` asynchronously after React has finished rendering.**

In other words `useEffect` doesn't run the moment you call it, it runs after React has completed rendering.

I will mention this point again twice in the coming section because I feel this is really important to understand.

<hr>

# Making API calls inside the `useEffect` hook

Quite often we make async requests to external APIs inside the `useEffect` hook. So, in this section we would observe the flow of our code in such a scenario.

```jsx
const UserInfo = ({ userId }) => {
  const [user, setUser] = React.useState(null);
  const [error, setError] = React.useState(null);

  console.log("%cBefore useEffect", "color: yellow");

  React.useEffect(() => {
    console.log("%cInside useEffect", "color: cyan");

    setError(null);

    (async function fetchUser() {
      if (!userId) return;

      try {
        const res = await fetch(
          `https://jsonplaceholder.typicode.com/users/${userId}`
        );
        const data = await res.json();

        if (!Object.entries(data).length) throw new Error("No data found");

        setUser(data);
      } catch (e) {
        setError("Something went wrong");
      }
    })();
  }, [userId]);

  console.log("%cAfter useEffect", "color: coral");

  if (error) return <p>{error}</p>;

  if (!user) return <p>Loading...</p>;

  if (user) return <pre>{JSON.stringify(user, null, 2)}</pre>;
};

const UserSearchForm = ({ setUserId }) => {
  const handleSubmit = (e) => {
    e.preventDefault();
    setUserId(e.target.elements.userId.value);
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="userId">User Id:</label>
      <input type="text" id="userId" placeholder="Enter User Id" />
      <button type="submit">Search</button>
    </form>
  );
};

const App = () => {
  const [userId, setUserId] = React.useState("");
  return (
    <main>
      <h1>Find User Info</h1>
      <UserSearchForm setUserId={setUserId} />
      {userId && <UserInfo userId={userId} />}
    </main>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
```

Consider the above example, our focus is on the `UserInfo` component that makes `async` request to an external API.

Initially there are NO logs from the `UserInfo` component because it's not yet mounted (_as the `userId` state is initially set to an empty string_).

<hr>

### Let's search for a user with `userId` of `1`.

> **Logs:**

> _Before useEffect
After useEffect
Inside useEffect
Before useEffect
After useEffect_

So, when you hit the search button, `setUserId` is called which causes a re-render and now for the first time the `UserInfo` component is rendered.

The `UserInfo` function is called and from there we have our first log "*Before useEffect*".

💡 Observation: Notice that the second log that we have is not "*Inside useEffect*" but its "*After useEffect*"

🚀 **This is because `useEffect` runs asynchronously after React has finished rendering.**

So, after the "After useEffect" log, React renders `<p>Loading...</p>` and then React calls the `useEffect` function.

Inside `useEffect` we get the "*Inside useEffect*" log printed. 

Then we have `setError(null)`, before you proceed further just think for a moment will this cause a re-render?

The answer is NO and its because `error` is currently `null` and its being set to `null`, which means the `error` state has not changed, so a re-render is not required (React is Smart folks!).

So, we move past `setError(null)` and then `fetchUser` is called, and once the `data` is fetched from the API, we call `setUser` with that `data` (_assuming there's no error_) which causes a re-render and because of which we get our last two logs printed.

Before we proceed to the next section I want you add one more log to the `UserInfo` component as shown below:

```jsx
console.log("%cAfter useEffect", "color: coral");

if (error) return <p>{error}</p>;
console.log("%cAfter error check", "color: crimson");

if (!user) return <p>Loading...</p>;

if (user) return <pre>{JSON.stringify(user, null, 2)}</pre>;
```

<hr>

### Let's now search for a user with `userId` of `a`.

I don't want you to observe any logs here because its same as before (_except from the one we just added_).

We did this because we wanted to set our `error` state to something other than `null`.

(Clear the console before moving to the next section)

<hr>

### Let's again search for a user with `userId` of `1`.

There are a lot more logs this time, let's knock them one by one.

> **Logs**

> _Before useEffect
After useEffect
Inside useEffect
Before useEffect
After useEffect
After error check
Before useEffect
After useEffect
After error check_

We already know why we have the first two logs, **but notice** that we didn't print the "_After error check_" log and this is because we still have the `error` state set to `null`, which again emphasises the same fact that `useEffect` is not called before React finishes rendering.

So, React first renders `<p>{error}</p>` and after that it calls the `useEffect` hook and we get the third log "_Inside useEffect_".

Now, this time when `setError(null)` is called, it will cause a re-render because `error` is not `null` currently.

So, because of the change in `error` state we get logs 4, 5 and 6. And this time since `error` is no more truthy, therefore we log "_After error check_".

And finally once the data is fetched from the API, we call `setUser(data)` which causes a re-render and we get the last three logs.

<hr>
That's It! 🤘
Hope, you found this useful and learned something new. Let me know your thoughts in the comments.