Feb. 22, 2025
There’s no such thing as an isomorphic layout effect
by Shane Friedman
Originally published to smoores.dev
So, recently,
React ProseMirror
added support for server-side rendering. If you read my post about how React ProseMirror works, you may already know that React ProseMirror relies fairly heavily on
React’s useLayoutEffect
hook for reading data
from the DOM after render. And if you’re familiar with server-side
rendering, you may be familiar with what happens when you render a
component that uses useLayoutEffect
on the
server:
It’s worth breaking down what this warning is actually trying
to communicate, because it’s not especially straightforward. To start, we should
review what useLayoutEffect
is actually for. Like other React hooks,
useLayoutEffect
provides a mechanism for managing side effects. In
particular, as the name implies, layout effects are meant to be side effects
that read from the DOM, usually for the purpose of modifying the layout of a
component. To allow this, React will execute a component’s render function,
commit the changes to the DOM, and then immediately run its layout effects
before the browser paints those DOM updates. This means that something like a
tooltip component can evaluate the position of its anchor in a layout effect,
update its state to reflect that position, and be re-rendered with that new
state, all without the user ever seeing the tooltip in the wrong place.
Now let’s walk through what happens when we server-side render a component like this. Below, we have an example application that uses a layout effect to position a tooltip:
Because we’re using a layout effect, this component will actually be rendered twice on mount, with both renders occurring before the DOM has even been painted once. The result is that the tooltip will be correctly positioned on the very first paint, with the user only ever visually seeing a DOM represented by the following HTML:
But what happens when we render this component on the server? There is no DOM on the server at all, so React never executes layout effects. Instead, the component is rendered exactly once, using the default values for our state:
This means that in a server-side rendered context, until the
client-side JavaScript bundle is loaded, parsed, and executed, the
user will be looking at the wrong UI. The tooltip will simply
be in the wrong place (at 0, 0
). It will look
broken!
This is precisely the issue that React was trying to warn us about.
Because effect hooks don’t execute on the server at all, server-side
rendered UIs that rely on them may appear broken until they’re
hydrated on the client. Following the link from the warning message
takes us to a GitHub Gist with two proposed solutions: replacing the useLayoutEffect
with a
useEffect
, and conditionally rendering the component
that uses useLayoutEffect
only on the client. For our
tooltip example, we should use the second option — it’s better to simply not
render the tooltip at all until the client-side JavaScript has a chance to run
and determine where it should be positioned.
Not all layout effects actually need to modify the layout,
though. React ProseMirror, for example, uses layout effects internally to
maintain ProseMirror’s view descriptor tree, which is roughly analogous to
React’s virtual DOM. Because this requires reading from the DOM, but not
modifying it, it’s actually safe to include in a server-side rendered component.
But it’s a huge pain to fill up users’ server logs with warnings about
useLayoutEffect
that they can’t (and don’t need to) do anything about!
If you’ve been around the server-side rendering block once or twice, you can probably see where this is going. The use-isomorphic-layout-effect library, or other implementations of it available from other popular libraries, is often the first tool that developers reach for when they encounter this warning. Let’s take a look at its implementation:
Very simple! The library only runs
useLayoutEffect
if the code is running on the
client (in the browser, this determined via
_typeof document !== "undefined"
).
On the server, instead, it runs… _useEffect
,
instead? That’s sort of odd. Effects never execute on the server — why
would we bother running useEffect
there?
And it’s not just this library that’s made this somewhat odd choice of no-op. Here’s react-use’s implementation:
The Mantine design system:
React Beautiful DnD:
In case it’s not clear why I’m so fascinated by this choice, here’s React ProseMirror’s implementation:
This implementation has precisely the same behavior as the
implementations above. On the client, it calls
useLayoutEffect
, and on the server, it does
nothing. I didn’t name it “isomorphic”, because it’s not really
isomorphic — at least in the sense of “Isomorphic JavaScript”, which
describes JavaScript code that runs on both the client and the server
— as it doesn’t run on the server at all!
Just to be clear, this doesn’t really matter. I’m not arguing
that no one should ever use use-isomorphic-layout-effect, or that all of these
libraries need to change their implementations of this function to use an
explicit no-op instead of useEffect
on the server. I am, however, curious
about where this surprisingly ubiquitous quirk of the React ecosystem came from.
And I have a hypothesis.
In February of 2019, the React team released React 16.8, the first stable release of React that included hooks. Two months later, React Redux released their v7, which included a new hooks-based integration between React and Redux. And wouldn’t you know it:
Make sure to read those comments — the React Redux team seems fully
aware that useEffect
is a mere no-op here.
React Beautiful DnD’s implementation actually directly references this
React Redux code. Other implementations likely either copied from one
of these two popular libraries, or from
this Medium post from a few weeks later.
From what I can tell, a very popular, well maintained library made an early, arbitrary implementation decision. Because copying this library felt like a safe bet to other library maintainers, this arbitrary decision became the de facto implementation for this workaround. A Medium post about this implementation became so widely read that it’s still the number one Google result for the query “useLayoutEffect ssr warning”, several slots above the GitHub Gist discussing the correct solution for most use cases.
Even though I had an explanation, this kept itching at me. This is partly due to the description of the use-isomorphic-layout-effect library, which reads:
A React helper hook for scheduling a layout effect with a fallback to a regular effect for environments where layout effects should not be used (such as server-side rendering).
There is no mention here that useEffect
is a
mere no-op in those situations. It also seems to describe the problem
space somewhat incorrectly — if a given layout effect actually should
not be used in server-side rendering, then the component using it
almost certainly should not be server-side rendered at all. Falling
back to a plain effect in that situation is precisely as incorrect as
using a layout effect — only without a warning to guide you toward the
correct solution.
react-use’s useIsomorphicLayoutEffect
hook
has a somewhat more accurate description:
useLayoutEffect
that does not show warning when server-side rendering, see Alex Reardon’s article for more info.
But it also lacks any detail about when it’s appropriate to use this
hook in place of useLayoutEffect
. And, worse,
on the main README for react-use, the description for the hook reads:
useLayoutEffect
that that [sic] works on server.
Which is not correct. This hook, like all other “isomorphic” layout
effect hooks, has exactly the same behavior as
useLayoutEffect
, minus the warning. It does
not work on the server!
I may be reading far too much into this very scant story, but I began to see a narrative unfold the further I looked into this:
A maintainer for a very popular open source library, in the midst of a big refactor, made an essentially arbitrary decision to work around a noisy warning that wasn’t relevant to their use case. They seem to have done this with full knowledge that their decision was arbitrary, and left a comment explaining it.
Another maintainer for a similarly popular open source library also needed to work around the warning, which was similarly irrelevant to their use case. They saw this workaround and decided to copy it as-is, leaving only link to the original (which has since been replaced) as explanation.
A developer, frustrated by the warning, found these libraries’
workaround and authored a short blog post touting it as a way to quiet
the warning. They seem to at least somewhat misunderstand the purpose
of the warning (or maybe they fully understand it, but didn’t fully
explain), and don’t clarify in their post that the choice of
useEffect
is essentially arbitrary.
As more developers migrated to use React hooks, more developers ran into this warning and began searching for solutions. Some of them published the solution from Reardon’s blog post in their own libraries, and others found Reardon’s post and implemented his approach themselves.
At each step in the saga, there’s less and less context. Even though the warning itself links to a GitHub Gist that explains the issue and solutions quite well, searching the language of the warning will retrieve Reardon’s post and other solutions before the linked Gist from the React team.
As a result, the de facto solution to this “problem” doesn’t have
sufficient context for users to understand how to use it effectively.
The hugely popular
React-Select library, for example,
incorrectly uses use-isomorphic-layout-effect
to position and scroll a menu, when it should instead avoid rendering
the menu on the server at all. And I’m not trying to pick on
React-Select — it seems likely that this is almost never an actual bug
for them, since menus are likely always collapsed during the server
render. But that is precisely the use case that the React team had in
mind when they added the useLayoutEffect
warning!
To me at least, this is a reminder of why it’s important to understand why our code does what it does. It can be tempting to sit back and let sleeping dogs lie after finally finding the solution to a confounding bug. But it’s all too easy for those incomplete understandings to build up and slowly shift our intuition over time, until we find that our mental model of our problem space doesn’t match reality any longer.
Oh, and React ProseMirror doesn’t trigger the layout effect warning during server-side rendering anymore!