useEffect vs useLayoutEffect in plain, approachable language

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 useEffectfunction 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 onlyafter the DOM changes are painted to the screen.

You’ll find the official docs put it this way: the function passed to useEffectwill 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 useEffectand 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 useLayoutEffectproduces 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.

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.

Thanks for reading. If you liked this post, share it with all of your programming buddies!

Further reading

How to add Google sign-in to your Web app

☞ React - The Complete Guide (incl Hooks, React Router, Redux)

☞ Modern React with Redux [2019 Update]

☞ The Complete React Developer Course (w/ Hooks and Redux)

☞ React JS Web Development - The Essentials Bootcamp

☞ React JS, Angular & Vue JS - Quickstart & Comparison

☞ The Complete React Js & Redux Course - Build Modern Web Apps

☞ React JS and Redux Bootcamp - Master React Web Development


Originally published on blog.logrocket.com 


#reactjs #react-native #javascript #web-development

useEffect vs useLayoutEffect in plain, approachable language
1 Likes31.05 GEEK