Demystifying React Hooks — useLayoutEffect
In this article, we will explore when and how to use React's useLayoutEffect
hook and how it relates to the useEffect
Hook.
If you'd like to follow along in your local IDE, you can find the GitHub Repo here.
Getting Started
fork and clone
cd client
npm i
npm start
Creating the Problem
Sure, useLayoutEffect
is not something you'll use often. It actually took me longer to think of a use case than to code out the example. It's kinda like the US Supreme Court's definition of obscenity, "You know it when you see it."
The React beta docs offer a cool, albeit incredibly specific use case with "a tooltip that appears next to some element on hover. If there's enough space, the tooltip should appear above the element, but if it doesn't fit, it should appear below"… if you'd like to check that out.
While you likely won't turn to useLayoutEffect
often enough to need a snippet, understanding how it works will help you better understand useEffect
, which you'll use daily as a React 18+ developer. And for the one time in your career when you need to pull it out, you'll know precisely the right tool for the job.
What is useLayoutEffect
?
As every good developer pretends to do before Googling, let's turn to the docs.
The first thing we see on the page for useLayoutEffect
is a bright orange warning telling us not to use it.
Got it.
Reading on, we find the following description:
useLayoutEffect
is a version ofuseEffect
that fires before the browser repaints the screen.
Sweet.
If you understand useEffect
, this is a pretty straightforward explanation. However, if you've found yourself here and don't have a firm grasp on useEffect
, I'd recommend reading up on theat and revisiting this article. And since you didn't take my advice, let's quickly recap useEffect
.
useEffect Recap
useEffect
calls an anonymous function after the values in its dependency array have changed. It is asynchronous and non-blocking. It runs after the browser has painted the screen and can optionally return a "clean-up" function. Essentially, useEffect
is all the old crusty component lifecycle methods rolled up into one tidy hook.
useEffect
's primary purpose is to asynchronously run side effects without blocking the in-browser JavaScript's Single Thread, offering a more performant and responsive application resulting in a more seamless User Experience.
In plain English, your React App won't wait on useEffect
to finish doing its thing before painting the DOM. Your virtual DOM will render and browser will paint while useEffect
runs alongside that process, and if your useEffect
affects the UI, it will only repaint what is needed.
Fetching data from an API and updating state is one of the most common use cases for useEffect
.
So how does this compare to useLayoutEffect
?
useEffect
vs. useLayoutEffect
useLayoutEffect
's signature is identical, but it is synchronous, blocking, and runs before the browser paints. So these two are exactly the same but exactly the opposite.
Starter Code
Let's finally dive into the code.
In App.js
, you'll see our entire app that aims to simulate a Super Exciting Live Stream with a chat functionality on the sidebar.
On lines 15 and 16, we're simply initializing a ref
called afterChatRef
and a state called messages
.
After our imports, we have a setInterval
function that counts to a billion every two milliseconds. React is getting better and better at outsmarting our clunky code, so this is purely a way to slow down the application. And the comment is just asking es-lint to cool it with the unused vars warning.
On line 19, getMessages
gets our data from a dummy API and sets that data to the messages
state.
On line 24, we have a useEffect
with an empty dependency array that calls our getMessages
function. So the component mounts, the virtual DOM is rendered and painted to the real DOM, then, when our useEffect
runs, it updates messages
which rerenders the virtual DOM, React compares the diff and causes the real DOM to repaint.
There’s another useEffect
on line 28, whose dependency array contains messages
. Since this hook depends on the value of messages
, it doesn't trigger until messages
changes. When called, this useEffect
scrolls the afterChatRef
into view.
Practically speaking, it takes the user to the bottom of the chat. There is no functionality to add new messages, but as you can imagine, when the user adds a message, state changes, and our useEffect
scrolls us to the bottom.
Nothing exciting in the JSX. Our "stream" is on the left, and we're conditionally rendering the messages
in a container on the right.
So What's the Problem?
Admittedly, were it not for my function sandbagging our app, we would use useEffect
like normal and not notice the issue. If your machine is significantly faster than mine, you still may not notice the problem. If that's the case, open your dev tools (shame on you for not already having them open), click the Performance
tab, click CPU No throttling
, and set it to 4x slowdown
or even 6x slowdown
, depending on how much richer you are than me.
Refresh the page. Nice and slow.
See how the chat loads and hangs at the top for a moment before snapping to the bottom? Let's reduce the for loop
to 1e5
(100,000) instead of 1e9
, save, and refresh.
This is closer to how it may look out in the wild. This flicker is that "obscenity" I was talking about earlier.
Let's refer back to the React doc's tooltip example to see the root issue they're trying to solve.
You don’t want the user to see the tooltip moving. Call
useLayoutEffect
to perform the layout measurements before the browser repaints the screen.
You don't want the user to see the tooltip moving. Or, say, to see the chat flickering before snapping to the bottom?
Implementing useLayoutEffect
This refactor will be significantly less work than previous articles. All we need to do is import useLayoutEffect
from 'react'
.
And replace our useEffect
on line 28 with useLayoutEffect
, leaving the useEffect on line 24 undisturbed.
Now, when we save and refresh, voila! Our chat no longer flickers at the top before scrolling to the bottom.
To demonstrate further, change our arbitrary loop back to 1e9
and throttle your browser if needed. Refresh.
While painfully slow, you can see the DOM doesn't paint the chat section until after the useLayoutEffect
has scrolled us to the bottom.
Our chat no longer flickers because, after the value of messages
changes, we're blocking the browser from painting the DOM until the useLayoutEffect
scrolls the chat to the bottom.
Some Cautions
No flicker. So why not always use useLayoutEffect
over useEffect
? Well, it's essential to understand what precisely is going on and why we should heed React's enormous orange warning.
Remember, useLayoutEffect
is synchronous and blocking. These are actually both standard function behavior JavaScrip in-browser. Synchronous means it runs from the top down line-by-line, and blocking means it is a resource hog that won’t return
until completed.
Because of this blocking behavior, we should only implement useLayoutEffect
when we notice the need. If we go around useLayoutEffect
-ing all over the place, we'll unnecessarily bog down our app and may as well be writing in vanilla JavaScript.
This is why the docs recommend useEffect
or useLayoutEffect
. We should approach useLayoutEffect
with the same opt-in mentality as useCallback
or useMemo
. Only implement it after you notice the need.
Preemptively optimizing our app will often introduce bugs and complicate the codebase with little benefit. In general, if you don't know whether you should implement useLayoutEffect
, you probably shouldn't.
Conclusions
useLayoutEffect
is a nifty little hook, and truthfully, you won’t use it very often. But if you do, it’s important to acknowledge the tradeoffs and make conscious sacrifices. Don’t go preoptimizing willy-nilly. As we used to say in the film industry, “Do nothing. Stay ahead.”
And hey, being able to discuss useLayoutEffect
intelligently might impress some fellow React nerds in your favorite comments section and even a Hiring Manager or two. But let’s circle back to the question that started this all. “When should I use useLayoutEffect
?”
You'll know it when you see it.
I’m always looking for new friends and colleagues. If you found this article helpful and would like to connect, you can find me at any of my homes on the web.