Since the introduction of Hooks in React 16.8, the way developers write their components has changed. Hooks arguably improve the developer experience and help you save time writing unnecessary code and boilerplate.
But in order to achieve such greatness, some abrupt changes were required. The well-established paradigm of class components was ditched, and lifecycle hooks, which semantically make a lot of sense, were removed.
It is vital to understand that there is no such thing as a 1:1 map between an old lifecycle hook and new React Hooks. Indeed, you can leverage them to achieve the same effect, but ultimately, they are not the same thing.
The goal of this article — in addition to stressing that writing Hooks requires a different mindset than writing class components — is to give you a better overview of how, exactly, the whole React component lifecycle works. That is to say, in what order the code is being executed in your app.
Even if you think you know the answer, bear with me. You might find surprising results throughout this text.
As always, let’s start with the basics. React, as its name suggests, is reactive to changes — namely, to changes in either its props or state. A prop is an external variable passed to a component, and a state is an internal variable that persists across multiple renders.
Both props and state variables cause a re-render when they change; from this, it follows that the only difference between a state and a ref is that a ref change does not cause a re-render. More about refs here.
Consider the following example:
const CustomButton = () => { console.log("I'm always called"); return <button>Click me</button>; }
The above component has no props (arguments to the function, i.e., the component) and no state. This means that, as it stands, it will render only once.
Now, consider the version with a prop:
const CustomButton = ({color}) => { console.log("I'm called with color", color); return <button style={{color}}>Click me</button>; }
If you render <CustomButton color="red" />
and then change it to <CustomButton color="blue" />
, you will see two logs, one for each time the function (component) was called.
Let’s now introduce the concept of state. Consider the whole application below:
const App = () => { const [count, setCount] = useCount(0); const buttonColor = count % 2 === 0 ? "red" : "blue"; console.log("I'm called with count", count); return ( <div> <CustomButton color={buttonColor} onClick={() => setCount(count + 1)} /> <p>Button was clicked {count} times</p> </div> ); } const CustomButton = ({color, onClick}) => { console.log("I'm called with color", color); return <button style={{color}} onClick={onClick}>Click me</button>; }
Now we have a nested component. We shall study how it reacts in several scenarios, but for now, let’s take a look at what happens when we mount <App />
, i.e., when the application is rendered:
I'm called with count 0 I'm called with color "red"
If you click on the button once, note that the console output will include:
I'm called with count 1 I'm called with color "blue"
And for a second click:
I'm called with count 2 I'm called with color "red"
What is happening? Well, every time you click on the button, you change App
‘s state. Because there was a state change (count
was incremented), the component re-renders (the function runs again).
In this new execution, once count
changes, buttonColor
will also change. But note that buttonColor
is passed as a prop to CustomButton
. Once a prop changes, CustomButton
will also re-render.
Before we dive deeper with Hooks, consider this extended version:
const App = () => { const [count, setCount] = useState(0); const buttonColor = count % 2 === 0 ? "red" : "blue"; console.log("I'm called with count", count); render ( <div> <CustomButton color={buttonColor} onClick={() => setCount(count + 1)} /> <CustomButton color="red" onClick={() => setCount(count + 1)} /> <p>Button was clicked {count} times</p> </div> ); } const CustomButton = ({color, onClick}) => { console.log("I'm called with color", color); return <button style={{color}} onClick={onClick}>Click me</button>; }
What do you think will happen now?
One fair observation would be to say that only the first CustomButton
, which has a variable color, will change. However, that’s not what we see in practice: both buttons will re-render. In this particular case, onClick
changes whenever count
changes, and as we saw, a prop change will trigger a re-render.
We shall see, however, that having static props and state alone aren’t enough to prevent a re-render.
Consider the following example:
const App = () => { const [count, setCount] = useCount(0); console.log("I'm called with count", count); render ( <div> <button onClick={() => setCount(count + 1)}>Increment</button> <p>Button was clicked {count} times</p> <StaticComponent /> </div> ); } const StaticComponent = () => { console.log("I'm boring"); return <div>I am a static component</div>; }
Notice that StaticComponent
has neither props nor state. Considering what we learned so far, one might imagine that it will render only once, and never re-render even if the parent App
changes. Again, in practice, that’s not what happens, and I'm boring
will be logged in the console every time the button is clicked. What’s going on?
Turns out that React re-renders every child of the tree whose parent is the component that changed. In other words, if A is the parent of B, and B is the parent of C, a change in B (e.g., a state variable being updated) will spare A from change but will re-render both B and C.
The following image from this great post by Ornella Bordino gives a good visual of the effect:
Refer to the above post if you are interested in the implementation details.
The ingenuous reader is now asking: Is that a way to prevent this behavior? The answer, as you might expect, is yes. React has a built-in function called memo
that basically memoizes the result of the component and prevents the re-render given that neither the props nor state change.
We could update the prior example to:
... const StaticComponent = React.memo(() => { console.log("I'm boring"); return <div>I am a static component</div>; });
…and voilà! Our StaticComponent
will render only once and never re-render, no matter what happens up in the tree.
A word of caution: React.memo should be used sparingly. Truth is, you may never need to use it at all! Its benefits will only show up for components that are large (so re-rendering them often is undesirable) and mostly static (because you do not want to make shallow comparisons every time). Use React.memo()
wisely is a fine piece that describe exactly when this technique should or should not be used.
The following CodeSandbox wraps-up our discussion about re-rendering. Notice that having React.memo()
only prevents a re-render when the props won’t:
Note: As of the time of this writing, there’s a bug in CodeSandbox when the component is called twice when there is a state change. In a production environment, this won’t happen.
Now that we have a clear understanding of when components (functions) are rendered (executed) in React, we are ready to investigate the order in which they are called. Consider the following example:
const Internal = () => { console.log("I'm called after") return <div>Child Component</div> } const App = () => { console.log("I'm called first"); return ( <Internal /> ); }
As you might expect, the console will log:
I'm called first I'm called after
Now, we know that useEffect
Hooks are called after the component is mounted (more about useEffect
here). What do you expect will be the log for the following?
const Internal = () => { console.log("I'm called after") const useEffect(() => { console.log("I'm called after Internal mounts"); }); return <div>Child Component</div> } const App = () => { console.log("I'm called first"); const useEffect(() => { console.log("I'm called after App mounts"); }); return ( <Internal /> ); }
Surprisingly:
I'm called first I'm called after I'm called after Internal mounts I'm called after App mounts
This happens because useEffect
is called in a bottom-up fashion, so the effects resolve first in the children, and then in the parent.
What do you think will happen if we add a callback ref?
const Internal = () => { console.log("I'm called after") const useEffect(() => { console.log("I'm called after Internal mounts"); }); return <div>Child Component</div> } const App = () => { console.log("I'm called first"); const useEffect(() => { console.log("I'm called after App mounts"); }; return ( <Internal ref={ref => console.log("I'm called when the element is in the DOM")} /> ); }
The result:
I'm called first I'm called after I'm called when the element is in the DOM I'm called after Internal mounts I'm called after App mounts
This is tricky but important: it tells us when we can expect to access an element in the DOM, and the answer is after components are rendered but before the effects run.
useEffect
orderWe have seen that useEffect
runs after the component mounts. But in which order are they called?
const App = () => { console.log("I'm called first"); const useEffect(() => { console.log("I'm called third"); }; const useEffect(() => { console.log("I'm called fourth"); }; console.log("I'm called second"); return ( <div>Hello</div> ); }
No surprises here. Everything outside the effects will run first, and then the effects will be called in order. Notice that useEffect
accepts a dependency array, which will trigger the effect when the component first mounts and when any of the dependencies change.
const App = () => { const [count, setCount] = useState(0); const [neverIncremented, _] = useState(0); console.log("I'm called first"); const useEffect(() => { console.log("I'm called second on every render"); }; const useEffect(() => { console.log("I'm called only during the first render"); }, []; const useEffect(() => { console.log("I'm called during the first render and whenever count changes"); }, [count]; const useEffect(() => { console.log("I'm called during the first render and whenever neverIncremented changes"); }, [neverIncremented]; return ( <div> <p>Count is {count}</p> <button onClick={() => setCount(count + 1)}> Click to increment </button> </div> ); }
If you run the above code, the output will be:
I'm called first I'm called second on every render I'm called only during the first render I'm called during the first render and whenever count changes I'm called during the first render and whenever neverIncremented changes
Finally, if you click on the button:
I'm called first I'm called second on every render I'm called during the first render and whenever count changes
Callback refs can have unexpected behavior. We’ve seen before that they are called between components rendering and effects running. But there’s a special case when the component is updated:
const App = () => { const [count, setCount] = useState(0); console.log("I'm called first"); return ( <div> <p>Count is {count}</p> <button onClick={() => setCount(count + 1)} ref={ref => { console.log("I'm called second with ref", ref); }}> Click to increment </button> </div> ); }
When the component first renders, the output will be:
I'm called first I'm called second with ref <button>Click to increment</button>
Which is precisely what you expect. Now check what happens when you click on the button and trigger a re-render:
I'm called first I'm called second with ref null I'm called second with ref <button>Click to increment</button>
The callback is executed twice, and the worst thing about it is that the ref is null
during the first execution! This is a common source of bugs when users programatically want to trigger some DOM interaction when a state changes (for example, calling ref.focus()
). Check out a more detailed explanation here.
The following CodeSandbox summarizes what was explained in the previous sections:
React Hooks are great, and their implementation is straightforward. However, to master a component’s lifecycle is not trivial, especially because old lifecycle hooks were deprecated. With patience, however, one can understand exactly what is going on in a React tree and even optimize if it ever comes to be a problem.
Personally, I have been working exclusively with the Hooks API since its first release and still find myself confused from time to time. Hopefully this article will serve as a guide not only to me, but to you as you get more comfortable and experienced with the marvelousness of Hooks.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowconsole.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
See how to implement a single and multilevel dropdown menu in your React project to make your nav bars more dynamic and user-friendly.
NAPI-RS is a great module-building tool for image resizing, cryptography, and more. Learn how to use it with Rust and Node.js.
3 Replies to "The post-Hooks guide to React call order"
Does it compile with `const useEffect` ? Because useEffect is a function that you are calling
Some of the App components in the Basics section have mistakenly used ‘render’ instead of the ‘return’ keyword. I’m assuming just a typo – great contents from the Log Rocket blog as always.
Thanks for reading the LogRocket blog, and for letting us know about the typo. We’ve just fixed it.