Introduction
Back in the previous React Foundation unit, recall that we covered effects in React in great detail and, in this regard, saw the useEffect()
hook which is used to work with side effects in components.
React provides a similar kind of hook to work with side effects in components but the way it's put into action differs from useEffect()
. That hook is useLayoutEffect()
.
In this chapter, we shall see what is a 'layout effect' in React; what is the useLayoutEffect()
hook; why might we even need it; the difference between useEffect()
and useLayoutEffect()
; and much more on this road.
We'll consider a couple of examples and also explore a couple of internal technical details underlying useLayoutEffect()
which are key to understanding its behavior when amalgamated with state mutations.
Layout effects aren't used very commonly in React applications but they sure represent an important concept to master and to leverage when the appropriate situation arises.
What are layout effects?
It's not impossible but quite improbable to discuss useLayoutEffect()
without talking a little bit about the useEffect()
hook.
Let's start off by considering the similarity between both these hooks.
Both useLayoutEffect()
and useEffect()
(as we learnt in React Effects) are used to perform side effects inside components.
Recall from the React Effects chapter that components in React are purely meant to directly contain presentational logic (JSX, conditionals, etc.); they are NOT meant to directly contain logic related to side effects.
For example, if we wish to setup a timer for a component, we'd do that as follows using useEffect()
:
function Greeting() {
useEffect(() => {
setTimeout(() => { ... }, 1000);
});
return (
...
);
}
NOT as follows:
function Greeting() {
// This shouldn't be here!
setTimeout(() => { ... }, 1000);
return (
...
);
}
And if you remember, that's essentially where the term 'effect' in useEffect()
comes from — the hook allows us to work with side effects within components.
In a similar vein, useLayoutEffect()
is also meant to perform side effects inside components.
With the similarity clear, let's now talk about the distinction between useLayoutEffect()
and useEffect()
, which is far more crucial.
If we focus upon the name of the useLayoutEffect()
hook, we might propose that it has something to do with the layout. Is that it? Is useLayoutEffect()
really tied to the layout?
Well, guess what, the name does convey some of the idea behind the hook.
useLayoutEffect()
is used to perform side effects — or as we call them, layout effects — that are somehow related to the layout of the underlying document (or in other words, the styles of the underlying document).Layout effects are performed synchronously in React right after the component's corresponding element node is injected into the DOM, typically to perform style computations involving elements from this DOM tree, but before the next repaint.
useEffect()
is different from this; it runs asynchronously (mostly). That is, useEffect()
runs only after the DOM tree is injected into the document and also painted on the screen.
The illustration below helps us visualize the place of useLayoutEffect()
in the rendering lifecycle of a component instance in React:
useLayoutEffect()
in a component's rendering lifecycle.- To render a component, first the component is called and then its return value (which is a set of rendered elements) is stored and processed into a set of element nodes.
- This set of element nodes is used to mutate the browser DOM.
- Right after the DOM mutation,
useLayoutEffect()
is invoked synchronously, before the browser repaints the screen.
This is the most important point, in fact the whole crux of useLayoutEffect()
, i.e. it's called right after the DOM is updated and before the browser repaints the screen.
Coming back to the illustration:
- Once
useLayoutEffect()
completes, the browser repaint routine executes. - Finally after that,
useEffect()
fires.
useEffect()
isn't always put into action after the repaint; sometimes it could be performed before the repaint (we'll see this in more detail below). But useLayoutEffect()
always gets performed before the repaint.Now a question that stands in line to be addressed right at this point is that: Why would we ever need to run something after the DOM is updated and before the repaint happens?
After all, if we could answer this question, then we'd know really well as to when would we ever need to employ useLayoutEffect()
. The good news is that the answer is very simple.
Purpose of useLayoutEffect()
Let's restate the question whose answer we seek: Why would we ever need to run something after the DOM is updated and before the repaint happens?
Well, sometimes, we might want to perform computations on DOM elements, and a couple additional operations as well, before they are ultimately repainted by the browser on the screen.
For example, let's take a simple <div>
element. As soon as we insert it into the DOM, we might want to figure out its height and then set the height to 0px
before it's finally painted on the screen. This might be required if we wish to animate the height of the <div>
.
Now figuring out the height and then setting it to 0px
before the <div>
appears on the screen is precisely something that we need to do immediately after the DOM is updated and before the browser repaint happens.
And this is just one instance of this need; there are possibly many others.
In normal JavaScript, we perform these computations immediately after we insert DOM nodes into the document (using appendChild()
, insertBefore()
, or any other element insertion method). Only once all these computations complete does the browser's main thread finally get the chance to be able to perform the extremely crucial repaint routine.
But when we enter React, since we don't strictly control the exact moment when our elements are inserted into the DOM, we don't exactly know when to perform these computations until we meet...you guessed it...useLayoutEffect()
.
useLayoutEffect()
is used to perform side effects in components that require us to obtain stylistic information from the underlying DOM elements (before they are painted) and use those to ultimately shape the final styles of those elements.
Believe it, it's much simpler than it sounds.
Syntax of useLayoutEffect()
As useLayoutEffect()
is similar to useEffect()
, it's not surprising to know that their syntax is the same as well.
That is, useLayoutEffect()
also takes in two arguments, as follows:
useLayoutEffect(callback, dependencies)
- The first
callback
argument specifies a callback function to execute. - The second
dependencies
argument specifies a list of dependencies that should all change in order get the callback function to be executed.
With the syntax seen, let's explore the useLayoutEffect()
hook in more detail.
A simple example
Let's see an actual example to help clarify all the technical discussion we've been having thus far on useLayoutEffect()
.
The example we'll be using is the same <div>
example that we talked about above. That is, we need to insert a <div>
into the DOM, then determine its height (because we don't know of it), and finally set it back to 0px
before painting the <div>
on the screen.
We'll add a little bit of spice to this example — a button will serve to add the <div>
to the DOM.
Here's our initial setup with the <div>
and the <button>
:
import { useState, useRef } from 'react';
function App() {
const [shown, setShown] = useState(false);
const divRef = useRef();
return (
<>
<button onClick={() => setShown(true)}>Show div</button>
{shown && <div className="box" ref={divRef}>This is a div</div>}
</>
);
}
Because we need to access the actual DOM node for the <div>
, we obviously need a ref (referred to as divRef
).
When the button is clicked, here's what we want: the <div>
shows up in the DOM and then smoothly slides down.
First off, for the smooth effect, we'll add a transition
to the <div>
, as shown below, in addition to a bunch of other properties so that we can clearly visualize it:
.box {
transition: 2s ease;
background-color: yellow;
font-size: 40px;
overflow: hidden;
}
overflow: hidden
style is important here. Without it, if we set the <div>
's height to 0px
, the content inside it would still keep showing.So far, so good.
The button's click handler will set the shown
state to true
in order to display the <div>
. But since we need to set the height of the <div>
to 0
before it gets rendered, we need useLayoutEffect()
.
Here's the code using it:
import { useState, useRef, useLayoutEffect } from 'react';
function App() {
const [shown, setShown] = useState(false);
const divRef = useRef();
useLayoutEffect(() => {
if (shown) {
const height = divRef.current.offsetHeight;
divRef.current.style.height = '0px';
setTimeout(() => {
divRef.current.style.height = `${height}px`;
});
}
});
return (
<>
<button onClick={() => setShown(true)}>Show div</button>
{shown && <div ref={divRef}>This is a div</div>}
</>
);
}
Notice how we first extract the height of the <div>
in useLayoutEffect()
, using the offsetHeight
property of the <div>
, and then set it to 0px
.
Had we initially set the <div>
to a 0px
height using the style
property, we would've gotten back this same 0px
height when inspecting the element's height.
So, in order to get back the real height, we need to hold ourselves from setting any height
style on the <div>
before we obtain its height. Only once the height is obtained can we change then it to 0px
.
The height is stored in a height
constant and then applied in the end to the <div>
with the help of a simple timeout.
Let's try running this program and see what we get:
Wow! It actually looks amazing.
useEffect()
would produce the same effect in this case!
There's a subtle implementation detail worth noticing about useEffect()
because of which, surprisingly enough, replacing useLayoutEffect()
in the code above with useEffect()
would produce the exact same result.
Yes that's right — the same result with useEffect()
!
You can even confirm it as follows, where we have the same program rewritten above using useEffect()
instead of useLayoutEffect()
:
This subtle implementation detail of useEffect()
is as follows:
useEffect()
is called due to an event's occurrence, it is fired synchronously, before the next repaint as well, akin to useLayoutEffect()
(but after the execution of useLayoutEffect()
).This might suggest that useEffect()
and useLayoutEffect()
are interchangeable when they are fired by virtue of an event's occurrence.
So is that right? No.
In particular, when useEffect()
and useLayoutEffect()
are fired by virtue of an event's occurrence, sure useEffect()
gets performed synchronously before the next repaint but state changes inside it are handled asynchronously.
This is unlike state changes happening inside useLayoutEffect()
, which are all completed synchronously before they stop happening.
This behavior of useLayoutEffect()
is purely to make sure that the rendered output isn't in an inconsistent state when we make state changes inside useLayoutEffect()
.
The blocking nature of useLayoutEffect()
Everyone says that useLayoutEffect()
can hurt performance if there is anything computationally intensive happening inside it.
So is that right?
Well it sure is, and the reason is already in front of us from our previous discussion.
useLayoutEffect()
is called synchronously after the DOM is updated and before the browser repaint occurs. Likewise, if there is some intensive operation being carried out inside it, our browser's most crucial routine, i.e. the repaint routine, would cease to execute for all that precious time.
And in turn, this would make the experience of the application laggy, janky, and unresponsive — things every technology user frowns upon these days.
Thus we say that useLayoutEffect()
is blocking in nature. We are advised to use it only when it really makes sense to use it.
Let's even showcase a concrete example of this blocking nature of useLayoutEffect()
.
Suppose we have a button to display a piece of text, represented using a Paragraph
component, as follows:
function Paragraph({ children }) {
useLayoutEffect(() => {
var t = new Date();
while (new Date() - t < 2000);
});
return (
<p>{children}</p>
);
}
function App() {
const [shown, setShown] = useState(false);
return (
<>
<button onClick={() => setShown(true)}>Show div</button>
{shown && <Paragraph>This is a paragraph.</Paragraph>}
</>
);
}
The Paragraph
component has a useLayoutEffect()
hook in place with a deliberate, blocking while
loop, halting execution for a time span of 2 seconds.
Most importantly, when we click the button to display a <Paragraph>
element, we notice a 2s delay before the Paragraph
is painted on the screen.
During this 2s time, we can't interact with the program, for none of our interactions will be given any attention to while the main thread is occupied.
This basic example is reminiscent of the fact that any intensive operation inside useLayoutEffect()
could cause the application's fluidness to suffer if not coded and tested thoroughly and properly.
Long story short, use useLayoutEffect()
wisely!
State changes inside useLayoutEffect()
The last thing left to be discovered in this chapter is a scenario where the state is mutated from within useLayoutEffect()
. This is vital to be explored because, as we shall see, React has an interesting take on this.
First, let's state the interesting take of React and then present an example to help clarify it further.
Suppose we are inside useLayoutEffect()
and we mutate the state of the underlying component. Now what?
Should React perform the repaint (obviously after the completion of useLayoutEffect()
's callback) and then afterwards re-render the component with the updated state data?
Or should React prevent the subsequent repaint and instead first re-render the component with the updated state data and then perform the repaint?
Which one makes more sense and why?
We strongly urge you to think through this because the path that React takes is the most sensible and logical one out of these two. It would be a great exercise for you to ponder on this implementation detail.
Which of the two approaches shown above makes more sense?
Well, assuming you're done with your thinking, let's get to the answer.
When a state change happens inside useLayoutEffect()
, we are telling React that something related to the component has changed and that might possibly have an effect on its final output as well (in fact, it typically does have an effect).
So, in order to prevent any inconsistent output from being painted on the screen (be that even for a split millisecond), without updated state data, React decides to prevent the repaint routine from firing after a useLayoutEffect()
that has a state change.
And how exactly does React prevent the repaint? By synchronously re-rendering the component.
The illustration below helps us understand this much clearly and quickly:
useLayoutEffect()
's synchronous behaviorAs depicted here, when a state change occurs inside useLayoutEffect()
, the browser repaint is prevented while the entire sequence of rendering is repeated.
That is, following a state change inside useLayoutEffect()
:
- The underlying component is re-rendered (synchronously, after the current
useLayoutEffect()
call ends). - Then the respective DOM mutations (if any) are applied.
- And then
useLayoutEffect()
is called again to make sure that there are no remaining state changes.
In contrast, if we make the same state change inside useEffect()
, it lets the subsequent repaint from happening. But useLayoutEffect()
stalls the repaint until state changes stop from happening inside it.
To reiterate on it, useLayoutEffect()
takes this behavior in order to prevent any inconsistent output from being shown to the user with stale state data.
React wants to make sure that if any state mutation occurs because of useLayoutEffect()
, the updated state data must first be delivered to the component and only then should the rendered output of the component be painted on the screen.
And this makes 110% sense. (But using 110% instead of 100% doesn't make sense, or does it?)