Debugging React SSR Patching Problems
When using React 16, an issue that I've run into pretty frequently when using Server Side Rendering (SSR) is messed up styling. That may sound like a pretty vague thing, but generally this is accompanied by an obscure warning that doesn't seem related, something like:
There's likely terms in this post that could use a definition... let me know which ones! I probably won't add it to this post for scope's sake, but I could find/write one and link to it 😊
Messed up styles... a Next.js issue?
A prime example of messed up styling was brought up by my coworker, Zack, who had filed an issue with Next.js about styles not being correct, and then asked in our work slack if anyone wanted to dig into it. I looked at his repo that has a reproduction of the issue to see what might be happening. I've pulled it into a CodeSandbox example to make it easier to see a running version.
The crux of the issue that was reported is that the styling for the first text block, which is only rendered on the client (determined by checking to make sure window is defined) isn't correct when the page is server side rendered (it should have a blue background). If you don't really care why this happens, and just want to know what you can do try the following:
- Use semantic elements
- Try a better client-only render strategy to avoid a hydration mismatch
- Avoid server rendering DOM elements too eagerly
There's more details on these suggestions later in the post, but first more into why it happens.
Actually a React v16 issue
After digging into it I found that it's a React issue that’s been around a long
time, caused by having server/client mismatches like this. Here's the
original React issue, which
led to an issue about a still-not-finished blog post
on the docs site. In the
reproduction, the client side hydration thinks the first div is for the Test
component since that’s the first thing in the client-side structure. On the
client Test
is supposed to be included in the render (since window is now
defined), so React tries to fix the mismatch of wanting Test
by re-using the
DOM elements that already exist. React patches the text but not the attributes
when it re-uses the DOM elements, then appends a new Test2
on the end to match
what it expects.
From the React docs on Hydration:
React expects that the rendered content is identical between the server and the client.
When there is a mismatch, you get a warning... like the one in the screenshot above. They're not super helpful usually since the Client side React doesn't have knowledge of what Server side React was working with when it rendered. Client side React tries it's best with the output from the server to make the warnings useful... but as you saw in the first screenshot there's not much to work off of.
Avoiding the issue (or fixing it if you have it)
In order to avoid this issue there's a few things you can do:
- Use semantic elements
- Try a better client-only render strategy to avoid a hydration mismatch
- Avoid server rendering DOM elements too eagerly
I'd first recommend avoiding div
elements where possible, since React won't
try to reuse an element if it doesn't match. In most applications using semantic
elements could probably avoid the issue entirely, but in the cases where it
doesn't there's a few more things to do.
Using a better client side only render strategy, as described in the React docs on hydration is a valid solution to this issue. The docs say:
If you intentionally need to render something different on the server and the client, you can do a two-pass rendering.
To accomplish a "two-pass rendering", I generally reach for an isMounted
state
variable that starts out false
and is changed to true
in a useEffect
hook.
Something like:
const ClientOnlyComponent = () => {
const [isMounted, setIsMounted] = React.useState(false)
React.useEffect(() => setIsMounted(true))
return isMounted ? <ClientSideStuff /> : <WhatGetsRenderedServerSide />
}
I've not seen tons of cases where this is the approach that you should take, but it is particularly useful in cases where you don't have anything to render server side.
Finally, you should also check the output of your Server Side Render
occasionally. My favorite way to do this is through a right-click "View Page
Source" which should give you the HTML as it was from the server. I've
frequently seen extra div
elements in this output that have missing content
because there was a null check that excluded Another issue that causes this to
come up often is "extra divs" in the SSR due to null checks or other issues.
Something to watch out for would be nested empty divs like:
<div class="xyz">
<div class="abc"></div>
</div>
These div
elements are prime candidates for React to reuse and cause issues
with, so if you have a component that doesn't actually render anything on the
server I'd highly recommend avoiding the wrappers being rendered either.
A comment on the React issue has a great real-world example:
In this case they were trying to use a className
to do mobile/desktop styles,
but the server doesn't know what the screen size is so it uses a set default,
which more than likely is the wrong one, causing an attribute mismatch but no
structural differences allowing reuse to occur.
What about key props?
In that comment, they mention trying to use a key
prop to tell React that it's
a different div. The reason why the key
prop wouldn't work in these scenarios
is because it isn't actually rendered into the HTML that is sent over the wire
from the server to the client, so the client-side hydration would have no
knowledge of what the server-side key
prop was to compare with. This would
cause React to attach the key
client-side, but wouldn't result in throwing out
the div
that the server had sent over.
Banner image courtesy of undraw.co