Today I learned the difference between useEffect and useLayoutEffect in React, and why the second one prints an ugly warning during server-side rendering.

useEffect(() => {
  /* non-paint-blocking effect */
}, [ /* dependencies */ ])
useLayoutEffect(() => {
  /* paint-blocking effect */
}, [ /* dependencies */ ])

useEffect and useLayoutEffect are basically the “async” and “sync” versions of the same hook; otherwise, they behave identically. useEffect runs in the background asynchronously while the render where its dependencies changed is allowed to render and paint for the user to see, whereas useLayoutEffect blocks the render where the dependencies’ values changed from being painted to the screen until after the body of useLayoutEffect runs (and presumably tweaks the DOM in some way).

Until today we were using useLayoutEffect to perform a redirect in a new app:

import { routes } from "routes"
import { useCurrentUser } from "src/context"
import { useLayoutEffect } from "react"
import { useRouter } from "next/router"

export default function Redirect() {
  const user = useCurrentUser()
  const router = useRouter()

  useLayoutEffect(() => {
    if (!user) {
    router.replace(routes.index({ accountId: user?.account_id.toString() }))
  }, [user, router])

Since we’re going to redirect the browser anyway, it makes sense to save it the trouble of painting the initial render to the screen, right? Not when we there is server-side rendering in the mix! When a useLayoutEffect is encountered by React running on the server, it displays an ugly warning:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer’s output format.

In fact, neither useEffect nor useLayoutEffect can have any effect on the server-rendered output that gets sent to the browser (since there is no actual DOM for these effects to tweak), but this warning is emitted only for useLayoutEffect because it is usually used to do something to the DOM before the user gets to see the rendered component. Server-rendering will cause that “untweaked” version of the rendered output to be sent to the browser and shown to the user before the client-side app fires up and runs the useLayoutEffect to tweak it. So React is warning us that the user may see an ugly/broken version of the UI that we don’t want them to see.

What’s useLayoutEffect good for?

Permalink to What’s useLayoutEffect good for?

When I shared this internally at Culture Amp, several people remarked that they couldn’t think of any real use cases for useLayoutEffect. They are indeed rare – more so every year as CSS grows more and more capable – but here’s a semi-contrived example:

Let’s say you wanted to let the browser lay out the “natural” height of a div based on its content, and then use JavaScript to animate it from zero to that “natural” height, so it appeared to grow vertically from its top edge, revealing its content as it went.

You wouldn’t want the user to see a “flash” of the full-height div before you set its height back to zero for the start of the animation. useEffect would display that flash, whereas useLayoutEffect would let you inspect the height of the rendered div and then adjust it before the user got to see it.

In this case, the warning from React would be telling us that the user would see the flash, because the full-height div is getting rendered on the server, and the height tweak won’t be applied by useLayoutEffect until the JavaScript starts up on the client – long after the user got to see the server-rendered version of the page.

The example given in the official docs is also good: it involves calculating the position of a tooltip based on the rendered dimensions of the element to which it is attached.

  • A good short video demonstrating the difference between useEffect and useLayoutEffect.
  • A good gist that explains the SSR issue and two available workarounds.