Written by Ohans Emmanuel✏️
Before you dismiss this as another “basic” React article, I suggest you slow down for a bit.
Assuming you really understand the difference between useEffect
and useLayoutEffect
, can you explain this difference in simple terms? Can you describe their nuances with concrete, practical examples?
Can you?
What you’re about to read is arguably the simplest take on the subject you’ll find anywhere on the internet. I’ll describe the differences between useEffect
and useLayoutEffect
with concrete examples that’ll help you cement your understanding for as long as is needed.
Let’s get started.
What’s the actual difference between useEffect
and useLayoutEffect
?
Sprinkled all over the official Hooks API Reference are pointers to the difference between useEffect
and useLayoutEffect
.
Perhaps the most prominent of these is found in the first paragraph detailing the useLayoutEffect
Hook:
“The signature is identical to
useEffect
, but it fires synchronously after all DOM mutations.”
The first clause in the sentence above is easy to understand. The signature for both Hooks are identical. The signature for useEffect
is shown below:
useEffect(() => {
// do something
}, )
The signature for useLayoutEffect
is exactly the same!
useLayoutEffect(() => {
// do something
}, )
In fact, if you go through a codebase and replace every useEffect
call with useLayoutEffect
, although different, this will work in most cases.
For instance, I’ve taken an example from the React Hooks Cheatsheet that fetches data from a remote server and changed the implementation to use useLayoutEffect
over useEffect
.
It still works!
So, we’ve established the first important fact here: useEffect
and useLayoutEffect
have the same signature. Because of this, it’s easy to assume that these two Hooks behave in the same way. However, the second part of the aforementioned quote above feels a little fuzzy to most people:
“… it fires synchronously after all DOM mutations.”
The difference between useEffect
and useLayoutEffect
is solely when they are fired.
Read on.
An explanation for a 5-year old
Consider the following contrived counter application:
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
// perform side effect
sendCountToServer(count)
}, [count])
<div>
<h1> {`The current count is ${count}`} </h1>
<button onClick={() => setCount(count => count + 1)}>
Update Count
</button>
</div> }
// render Counter
<Counter />
When the component is mounted, the following is painted to the user’s browser:
// The current count is 0
With every click of the button, the counter state is updated, the DOM mutation printed to the screen, and the effect function triggered.
I’ll ask that you stretch your visual imagination for a bit, but here’s what’s really happening:
1. The user performs an action, i.e., clicks the button.
2. React updates the count state variable internally.
3. React handles the DOM mutation.
With the click comes a state update, which in turn triggers a DOM mutation, i.e., a change to the DOM. The text content of the h1
element has to be changed from “the current count is previous value ” to “the current count is new value.”
4. The browser paints this DOM change to the browser’s screen.
Steps 1, 2, and 3 above do not show any visual change to the user. Only after the browser has painted the changes/mutations to the DOM does the user actually see a change; no browser paint, no visual change to the user.
React hands over the details about the DOM mutation to the browser engine, which figures out the entire process of painting the change to the screen. Understanding the next step is crucial to the discussed subject.
5. Only after the browser has painted the DOM change(s) is the useEffect
function fired.
Here’s an illustration to help you remember the entire process.
What to note here is that the function passed to useEffect
will be fired only after the DOM changes are painted to the screen.
You’ll find the official docs put it this way: the function passed to useEffect
will run after the render is committed to the screen.
Technically speaking, the effect function is fired asynchronously not to block the browser paint process. What’s not obvious from the illustration above is that this is still an incredibly fast operation for most DOM mutations. If the useEffect
function itself triggers another DOM mutation, this happens after the first, but the process is usually pretty fast.
N.B.: Although useEffect
is deferred until after the browser has painted, it’s guaranteed to fire before any new renders. React will always flush a previous render’s effects before starting a new update.
Now, how does this differ from the useLayoutEffect
Hook?
Unlike useEffect
, the function passed to the useLayoutEffect
Hook is fired synchronously after all DOM mutations.
In simplified terms, useLayoutEffect
doesn’t really care whether the browser has painted the DOM changes or not. It triggers the function right after the DOM mutations are computed.
While this seems unideal, it is highly encouraged in specific use cases. For example, a DOM mutation that must be visible to the user should be fired synchronously before the next paint. This is so that the user does not perceive a visual inconsistency. I’ll show an example of this later in the article.
Remember, updates scheduled inside useLayoutEffect
will be flushed synchronously, before the browser has a chance to paint.
The difference between useEffect
and useLayoutEffect
in examples
As stated in the sections above, the difference between useEffect
and useLayoutEffect
is in when they are fired. Even so, it’s hard to tangibly quantify this difference without concrete examples.
In this section, I’ll highlight three examples that amplify the significance of the differences between useEffect
and useLayoutEffect
.
1. Time of execution
Modern browsers are fast — very fast. We will employ some creativity to see how the time of execution differs between useEffect
and useLayoutEffect
.
In the first example we’ll discuss, I have a counter similar to what we considered earlier.
What differs in this counter is the addition of two useEffect
calls.
useEffect(() => {
console.log("USE EFFECT FUNCTION TRIGGERED");
});
useEffect(() => {
console.log("USE SECOND EFFECT FUNCTION TRIGGERED");
});
Note that the effects log different texts depending on which is triggered, and as expected, the first effect function is triggered before the second.
When there are more than one useEffect
calls within a component, the order of the effect calls is maintained. The first is triggered, then the second — on and on the sequence goes.
Now, what happens if the second useEffect
Hook was replaced with a useLayoutEffect
Hook?
useEffect(() => {
console.log("USE EFFECT FUNCTION TRIGGERED");
});
useLayoutEffect(() => {
console.log("USE LAYOUT EFFECT FUNCTION TRIGGERED");
});
Even though the useLayoutEffect
Hook is placed after the useEffect
Hook, the useLayoutEffect
Hook is triggered first!
This is understandable. The useLayoutEffect
function is triggered synchronously, before the DOM mutations are painted. However, the useEffect
function is called after the DOM mutations are painted.
Does that make sense?
I’ve got one more interesting example with respect to the time of execution for both the useEffect
and useLayoutEffect
Hooks.
In the following example, I’ll take you back to college, or any other bittersweet experience you had plotting graphs.
The example app has a button that toggles the visual state of a title — whether shaking or not. Here’s the app in action:
The reason I chose this example is to make sure the browser actually has some fun changes to paint when the button is clicked, hence the animation.
The visual state of the title is toggled within a useEffect
function call. You can view the implementation if that interests you.
However, what’s important is that I gathered significant data by toggling the visual state on every second, i.e., by clicking the button. This was done with both useEffect
and useLayoutEffect
.
Using performance.now
, I measured the difference between when the button was clicked and when the effect function was triggered for both useEffect
and useLayoutEffect
.
Here’s the data I gathered:
Uninterpreted numbers mean nothing to the visual mind. From this data, I created a chart to visually represent the time of execution for useEffect
and useLayoutEffect
. Here you go:
See how much later useEffect
is fired when compared to useLayoutEffect
?
Take your time to interpret the graph above. In a nutshell, it represents the time difference — which in some cases is of a magnitude greater than 10x — between when the useEffect
and useLayoutEffect
effect functions are triggered.
You’ll see how this time difference plays a huge role in use cases such as animating the DOM, explained in example 3 below.
2. Performing
Expensive calculations are, well, expensive. If treated poorly, these can negatively impact the performance of your application.
With applications that run in the browser, you have to be careful not to block the user from seeing visual updates just because you’re running a heavy computation in the background.
The behavior of both useEffect
and useLayoutEffect
are different in how heavy computations are handled. As stated earlier, useEffect
will defer the execution of the effect function until after the DOM mutations are painted, making it the obvious choice out of the two. (As an aside, I know useMemo
is great for memoizing heavy computations. This article neglects that fact, and just compares useEffect
and useLayoutEffect
.)
Do I have an example that buttresses the point I just made? You bet!
Since most modern-day computers are really fast, I’ve set up an app that’s not practical, but decent enough to work for our use case.
The app renders with an initial screen that seems harmless:
However, it’s got two clickable buttons that trigger some interesting changes. For example, clicking the 200 bars button sets the count state to 200.
But that’s not all. It also forces the browser to paint 200 new bars to the screen.
Here’s how:
...
return (
...
<section
style={{
display: "column",
columnCount: "5",
marginTop: "10px" }}>
{new Array(count).fill(count).map(c => (
<div style={{
height: "20px",
background: "red",
margin: "5px"
}}> {c}
</div> ))}
</section>
)
This is not a very performant way to render 200 bars, as I’m creating new arrays every single time, but that’s the point: to make the browser work.
Oh, and that’s not all. The click also triggers a heavy computation.
...
useEffect(() => {
// do nothing when count is zero
if (!count) {
return;
}
// perform computation when count is updated.
console.log("=== EFFECT STARTED === ");
new Array(count).fill(1).forEach(val => console.log(val));
console.log(`=== EFFECT COMPLETED === ${count}`);
}, [count]);
Within the effect function, I create a new array with a length totaling the count number — in this case, an array of 200 values. I loop over the array and print something to the console for each value in the array.
Even with all this, you need to pay attention to the screen update and your log consoles to see how this behaves.
For useEffect
, your screen is updated with the new count value before the logs are triggered.
Here’s a screencast of this in action:
If you’ve got eagle eyes, you probably caught that! For the rest of us, here’s the same screencast in slow-mo. There’s no way you’ll miss the screen update happening before the heavy computation!
So is this behavior the same with useLayoutEffect
? No! Far from it.
With useLayoutEffect
, the computation will be triggered before the browser has painted the update. Since the computation takes some time, this eats into the browser’s paint time.
Here’s the same action performed with the useEffect
call replaced with useLayoutEffect
:
Here it is in slow-mo. You can see how useLayoutEffect
stops the browser from painting the DOM changes for a bit. You can play around with the demo, but be careful not to crash your browser.
Why does this difference in how heavy computations are handled matter? Where possible, choose the useEffect
Hook for cases where you want to be unobtrusive in the dealings of the browser paint process. In the real world, this is usually most times! Well, except when you’re reading layout from the DOM or doing something DOM-related that needs to be painted ASAP.
The next section shows an example of this in action.
3. Inconsistent visual changes
This is the one place where useLayoutEffect
truly shines. It’s also slightly tricky to come up with an example for this.
However, consider the following screencasts. With useEffect
:
With useLayoutEffect
:
These were real scenarios I found myself in while working on my soon-to-be released Udemy video course on Advanced Patterns with React Hooks.
The problem here is that with useEffect
, you get a flicker before the DOM changes are painted. This was related to how refs are passed on to custom Hooks (i.e., Hooks you write). Initially, these refs start off as null
before actually being set when the attached DOM node is rendered.
If you rely on these refs to perform an animation as soon as the component mounts, then you’ll find an unpleasant flickering of browser paints happen before your animation kicks in. This is the case with useEffect
, but not useLayoutEffect
.
Even without this flicker, sometimes you may find useLayoutEffect
produces animations that look buttery, cleaner and faster than useEffect
. Be sure to test both Hooks when working with complex user interface animations.
Conclusion
Phew! What a long discourse that turned out to be! Anyway, you’ve been armed with good information here. Go build performant applications and use the desired hook where needed.
Want to see my (new) take on Advanced React Patterns with Hooks? Sign up for the waiting list!
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post useEffect vs. useLayoutEffect in plain, approachable language appeared first on LogRocket Blog.
Top comments (0)