React Hook Flow

7 mins read

Sometimes it’s essential to understand the order in which your code runs when you’re using react hooks. We’ll mainly consider the useState and useEffect hooks. This flow is better understandable and clear with an example so let’s use two simple functional components that utilize these hooks.
import React from "react";

function App() {
console.log("App: render start");

const [showChild, setShowChild] = React.useState(() => {
console.log("App: useState(() => false)");
return false;
});

console.log(`App: showChild = ${showChild}`);

React.useEffect(() => {
console.log("App: useEffect(() => {})");
return () => {
console.log("App: useEffect(() => {}) cleanup");
};
});

React.useEffect(() => {
console.log("App: useEffect(() => {}, [])");
return () => {
console.log("App: useEffect(() => {}, []) cleanup");
};
}, []);

React.useEffect(() => {
console.log("App: useEffect(() => {}, [showChild])");
return () => {
console.log("App: useEffect(() => {}, [showChild]) cleanup");
};
}, [showChild]);

const element = (
<>
<label>
<input
type="checkbox"
checked={showChild}
onChange={(e) => setShowChild(e.target.checked)}
/>{" "}
Show child
</label>
<div
style={{
padding: 10,
margin: 10,
height: 30,
width: 30,
border: "solid"
}}
>
{showChild ? <Child /> : null}
</div>
</>
);

console.log("App: render end");

return element;
}

function Child() {
console.log(" Child: render start");

const [count, setCount] = React.useState(() => {
console.log(" Child: useState(() => 0)");
return 0;
});

console.log(` Child: count = ${count}`);

React.useEffect(() => {
console.log(" Child: useEffect(() => {})");
return () => {
console.log(" Child: useEffect(() => {}) cleanup");
};
});

React.useEffect(() => {
console.log(" Child: useEffect(() => {}, [])");
return () => {
console.log(" Child: useEffect(() => {}, []) cleanup");
};
}, []);

React.useEffect(() => {
console.log(" Child: useEffect(() => {}, [count])");
return () => {
console.log(" Child: useEffect(() => {}, [count]) cleanup");
};
}, [count]);

const element = (
<button onClick={() => setCount((previousCount) => previousCount + 1)}>
{count}
</button>
);

console.log(" Child: render end");
return element;
}

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

You can find the live code at https://codepen.io/Yemisrach15/pen/YzgxpRO?editors=0011.

Ignoring the console logs, let’s go over what the code snippet does. The root or main entry point is App. This component renders a checkbox input with a label and Child component based on a condition. The Child component renders a button that increments a count state when clicked. Both components maintain some state, in the case of App whether to show child or not, showChild, and in the case of Child the number of times the button is clicked, count. They each also have three useEffects, one without dependency, one with empty dependency, one with one dependency.

From the behavior of useEffects, you may know that the callback function passed to each executes

  • on every render when there is no dependency
  • only on initial render when there is an empty dependency array
  • on initial render and on a dependency change when there is a non-empty dependency array

Also notice that each useEffect has a cleanup function which runs when the component unmounts.


Hoping everything is clear by now, let’s jump straight into the hooks flow. We will first take a look at what happens on initial load.

  1. The app starts rendering
  2. The callback passed to useState is called to compute the initial value of the state, showChild
  3. We can see that the state, showChild, has been updated to false
  4. It reaches the end of the App component just before returning the jsx.
  5. React updates the DOM with the returned jsx
  6. Then react asynchronously calls the callbacks passed to the useEffects one by one in the order we wrote them.

Now what will be the order of execution when we click on the show child checkbox?

 

 

 

  1. The showChild state will be updated which causes a re-render. This means App starts executing from the top again hence why we see the console log for “App: render start”
  2. Unlike the first render, the state initializer function doesn’t get called for the rest of the lifetime of the component except when the component is initially rendered
  3. We can see that the showChild state has been updated
  4. It reaches the end of the component and here Child component is called.
  5. The child rendering starts
  6. Callback for the state initialization is called since this is the initial rendering of Child. This callback sets count to zero
  7. We reach the end of Child
  8. The cleanup functions of App’s effects are called in order.
  9. Child‘s effects callbacks are run sequentially
  10. App‘s effects callbacks are run sequentially. Note that the useEffect with empty dependency didn’t not run as this is not the initial render of the component.

Next let’s see what changes in the execution order when we click on the button that increases the count.

 

 

  1. Because clicking on the button updates the state of Child, only the child is re-rendered
  2. We see that the count value has increased by one
  3. We reach the end of the component and react updates the DOM
  4. The cleanup functions for the effects with no dependency and non-empty dependency are called.
  5. Callbacks for effects are called in order. Also note that callback for empty dependency effect is not called as this is not the initial render of Child

The last scenario we’ll take a look at is when the show child checkbox is unchecked

 

  1. This action changes the state of App meaning the component and the children nested within it are re-rendered. This affects Child too.
  2. We see that showChild state is updated
  3. We reach the end of App
  4. Child is unmounted which consequentially runs the cleanups within all useEffects
  5. Cleanup functions of App are called
  6. Finally callbacks of effects for no dependency and non-empty dependency are run

Generalizing from all scenarios we’ve seen, the flow consists of

  1. Running state initializing functions
  2. Updating the DOM
  3. Running cleanup functions if it’s not an initial render
  4. Running effect callback functions. If the effect callback functions update a state, a re-render is triggered

Understanding the hook flow is something that never crossed my mind until I stumbled upon it. It helps in figuring out what causes re-renders and optimize your code to minimize it. You can find a useful diagram that shows the react hook flow at https://github.com/donavon/hook-flow and why you might not need a useEffect at https://react.dev/learn/you-might-not-need-an-effect.