When I Actually Needed useLayoutEffect() in React
Tags: react, performance, hooks
Published: 2022-12-27 03:07:33

---
title: When I Actually Needed useLayoutEffect() in React
published: true
date: 2022-12-26 05:00:00 UTC
tags: react, performance, hooks
canonical_url: https://macarthur.me/posts/when-i-needed-uselayouteffect-in-react
---

If you’ve worked with React for any length of time, you’ve probably heard of the infamous `useLayoutEffect()` hook. You’ve also probably never used it. That’s because for most cases, `useEffect()` covers the vast majority of use cases with a much lower chance of shooting yourself in the foot. In fact, the the React docs even explicitly encourage you to use the latter whenever possible to avoid performance gotchas.

The reason for this is because of when each hook fires its callback. The `useEffect()` hook fires _after_ a component renders and paints changes to the DOM; `useLayoutEffect()` fires _before, and does so synchronously._

This means if you’re not careful, your expensive callback could block the main thread, preventing any UI updates from being rendered for the user.

### An Illustration

Here’s a simple `Name` component. All it does is render a piece of `name` state with a default of “Bob.” But there’s also a `useEffect()` hook that’ll fire whenever that state changes. And as soon as it does, it updates the name to “Alex.” If you were to render this component in the browser, you _probably_ wouldn’t notice any sort of flash. Instead, all you’d see is “Alex” painted to the screen:

```
const Name = () => {
  const [name, setName] = useState('Bob');

  useEffect(() => {
    setName('Alex');
  }, [name]);

  return <div>{name}</div>;
};

// <Name /> seemingly _only_ paints "Alex" to the screen.

```

But technically, `useEffect()` is firing _after_ a full render has taken place, which could also mean after that render has been painted to the screen. We can force this behavior by putting a synchronous pause in place. This `Name` component will still behave the same way, but this time, the state update will be pushed back by a second (1000ms).

```
const pause = (ms) => {
  let time = new Date();

  while (new Date() - time <= ms) {}
};

const Name = () => {
  const [name, setName] = useState('Bob');
	
	// Fires _after_ a render has taken place.
  useEffect(() => {
    pause(1000);
    setName('Alex');
  }, [name]);

  return <div>{name}</div>;
};

```

If you [try it out this time](https://stackblitz.com/edit/react-vafxw6), “Alex” no longer immediately paints to the screen. Instead, “Bob” is first rendered, and then, after a second, “Alex” pops in.

![](https://macarthur.me/proxy/7c1700ed-b4f5-4e81-a615-f5a9b151ce5e)

Based on how `useEffect()` is documented, this checks out. It’s only firing after a full render has been committed and prepared to be painted. And so any last-second state changes or DOM modifications are at risk for being “flashed” to the screen before they’re desired.

Now, let’s make a small modification: instead of using `useEffect()`, we’ll use `useLayoutEffect()`. This time around, we’ll change the state _before_ any changes have been allowed to fully render to the DOM. And for that reason, we’ll see some far more predictable results:

```
const Name = () => {
  const [name, setName] = useState('Bob');

	// Fires _before_ anything's been rendered at all.
  useLayoutEffect(() => {
    pause(1000);
    setName('Alex');
  }, [name]);

  return <div>{name}</div>;
};

```

If you were to try it out now, you’ll see that the “Bob” flash is gone. Instead, nothing renders at all until our synchronous `useLayoutEffect()` callback has finished:

![](https://macarthur.me/proxy/e9a61e0d-823a-43bd-9c59-e5e4c0e34298)

## So, When Should I Use It?

When choosing between `useEffect()` and `useLayoutEffect()`, use the latter when you need to something _to_ or _with_ the DOM _before_ anything has been painted to the screen.

### My Own Scenario

I was building a simple tooltip from scratch. The component allows its positioning to be configured with a `position` prop — either right, center, or left. However, depending on the layout of the item its mounted to, as well as the dimensions of the viewport, I _sometimes_ might want to override the provided position with an `overridePosition`. Here’s an ultra-simplified version of this component and how it can be used.

```
const Tooltip = ({ children, message, position = 'center' }) => {
  const [shouldShow, setShouldShow] = useState(false);
  const toggle = () => setShouldShow((val) => !val);

  return (
    <span class="tooltip-wrapper">
      {shouldShow && <div className={`tooltip ${position}`}>{message}</div>}

      {children({ toggle })}
    </span>
  );
};

export default function App() {
  return (
    <Tooltip position="left" message="My message!">
      {({ toggle }) => {
        return <button onClick={toggle}>Toggle Tooltip</button>;
      }}
    </Tooltip>
  );
}

```

[This works](https://stackblitz.com/edit/react-dan16d), but there’s a problem: **we can predict neither the layout of the page nor the size of the user’s viewport.** And so, we might end up with the tooltip rendering like this — outside the view of the user:

![](https://macarthur.me/proxy/fbec16f9-f847-4613-abfb-a45a6de2f5b1)

In cases like these, there isn’t much React can do for us. Instead, we need to take the DOM into our own hands for a bit before any rendering takes place.

### Reading the DOM Before Anything’s Rendered

In order to get around this, we’re gonna update this component to set an `overridePosition` only if two conditions are met:

1. The tooltip’s DOM node itself is actually mounted.
2. That mounted node is hanging off the edge of the viewport.

Because all of this needs to take place before the user has a chance to see anything, it’s a great use case for `useLayoutEffect()`.

First, we’ll set up a `useLayoutEffect()` hook that checks if a `ref.current` value is truthy. That `ref` will be attached to the actual tooltip markup that’s conditionally rendered (via `shouldShow`):

```
const Tooltip = ({ children, message, position = 'center' }) => {
+	const ref = useRef(null);
	const [shouldShow, setShouldShow] = useState(false);
	const toggle = () => setShouldShow((val) => !val);

+	useLayoutEffect(() => {
+ if (!ref.current) return;
+	});

  return (
		<span className="tooltip-wrapper">
			{shouldShow && (
				<div 
					className={`tooltip ${position}`}
+ ref={ref}
				>
					{message}
				</div>
			)}

			{children({ toggle })}
		</span>
	);
};

```

It’s probably worth noting that we’re not passing any values to the dependency array of our hook. That’s because we _want_ the callback to run whenever any state changes. There could be countless other things going on in the browser between toggles, and so performing a fresh (yet-to-be-written) DOM check keeps us flexible with unpredictable layout changes.

Next up, we can check if the rendered tooltip is popping outside of the viewport. I’ll leave the implementation of that method (and other supporting methods) out of here, but if you’re interested, you can [check out the demo.](https://stackblitz.com/edit/react-dan16d)

```
const Tooltip = ({ children, message, position = 'center' }) => {
	const ref = useRef(null);
	const [shouldShow, setShouldShow] = useState(false);
	const toggle = () => setShouldShow((val) => !val);

	useLayoutEffect(() => {
		if (!ref.current) return;

+ // Don't bother unless the tip is clipped by the viewport.
+ if (!elementIsOutsideViewport(ref.current)) return;
 });

  return (
    <span className="tooltip-wrapper">
      {shouldShow && (
	      <div 
					className={`tooltip ${position}`}
					ref={ref}
				>
          {message}
        </div>
      )}

      {children({ toggle })}
    </span>
  );
};

```

And finally, if the element _is_ being clipped by the viewport, we can run through all possible positions (”left,” “center”, and “right”) to find the one that offers the most visibility. If that override is ever set, a small piece of state will be used to update the class instead of the `position` prop.

```
const Tooltip = ({ children, message, position = 'center' }) => {
	const ref = useRef(null);
	const [shouldShow, setShouldShow] = useState(false);
+	const [overridePosition, setOverridePosition] = useState('');
	const toggle = () => setShouldShow((val) => !val);

	useLayoutEffect(() => {
		if (!ref.current) return;
		if (!elementIsOutsideViewport(ref.current)) return;

+ const position = findMostVisiblePosition(ref.current);
+ setOverridePosition(position);
	});

	return (
		<span className="tooltip-wrapper">
			{shouldShow && (
				<div 
+ className={`tooltip ${overridePosition || position}`}
					ref={ref}
				>
					{message}
				</div>
			)}

			{children({ toggle })}
		</span>
	);
};

```

After all of that, the tooltip should behave much better, despite being provided a position that would cause it to render outside the viewport. And thanks to `useLayoutEffect()`, there’s zero risk of the incorrect position ever flashing to the screen.

![](https://macarthur.me/proxy/28c48ef5-bf80-46ce-9010-3f45d46e14a2)

## What Else Am I Missing Out On?

In digging into this, I came across _many_ hooks provided by React that I’ve never even heard of (ex: `useSyncExternalStore()`???). It’s incredible the seemingly niche use cases some of them cover, and the range in different problems they help to solve.

So, some homework for you: [skim through some of them](https://beta.reactjs.org/reference/react) the next time you’re waiting for a synchronous timeout to finish. You’ll be better off for it.

### Links

- [useLayoutEffect() Demo](https://stackblitz.com/edit/react-vafxw6)
- [Tooltip Demo](https://stackblitz.com/edit/react-dan16d)