React's hydration process is a crucial part of server-side rendering (SSR) where React attaches event handlers to the server-rendered HTML. However, sometimes we encounter situations where the server and client need to render different content, which can lead to hydration mismatches. In this blog post, we'll explore a neat trick to handle this scenario without triggering React's hydration errors.
The Problem
When using React with SSR, you might encounter situations where you need to render different content on the server versus the client. A common example is when we need to render something that depends upon browser cookies. React expects the server-rendered HTML to match exactly what would be rendered on the client during hydration. If there's a mismatch, React throws a hydration error:
Warning: Text content did not match. Server: "Bye World" Client: "Hello World"
Example Approach (That Causes Problems)
Here's a typical example that would cause hydration issues:
export default function Home() {
const isBrowser = typeof window !== "undefined";
return (
<div>
<h1>{isBrowser ? "Hello World" : "Bye World"}</h1>
</div>
);
}
This code will cause a hydration mismatch because the server renders "Bye World" (since window
is undefined), but the client renders "Hello World".
You could use useEffect here and render but that would cause a flicker.
The Solution: The Inline Script Trick
Here's where our trick comes in. We can use an inline script that executes immediately after the HTML is rendered but before React hydration begins. Here's how it works:
'use client'
const isBrowser = () => typeof window !== "undefined";
export default function Home() {
const inlineScriptFn = () => {
const helloElement = document.getElementById('hello');
if (helloElement) {
helloElement.textContent = 'Hello World';
}
}
return (
<div>
{isBrowser() ? <h1 id="hello">Hello World</h1> : <h1 id="hello">Bye World</h1>}
<script dangerouslySetInnerHTML={{ __html: `(${inlineScriptFn.toString()})()` }} />
</div>
);
}
Let's break down how this trick works:
- On the server, React renders
<h1 id="hello">Bye World</h1>
becauseisBrowser()
returnsfalse
. - Immediately after the HTML is sent to the browser, but before React hydration begins, our inline script executes:
- It finds the element with id "hello"
- Changes its content to "Hello World"
- When React begins hydration, it sees "Hello World" in both:
- The actual DOM (thanks to our script)
- The virtual DOM it wants to render (because
isBrowser()
is nowtrue
)
- No hydration mismatch occurs because the content matches!
Why This Works
This approach works because:
- The inline script executes synchronously before React's hydration process begins
- By the time React performs hydration, the DOM already contains the client-side content
- React sees matching content in both the real DOM and its virtual DOM
Conclusion
This hydration trick provides a clean solution to handle server/client content differences without triggering React's hydration warnings.
At Builder.io, we rely on this very inline-script hydration trick to select the winning variant in our SDKs. You can see the production implementation here: inlined-fns.ts.
I first discovered the technique while building "Variant Containers" in our SDKs (block-level personalization container), essentially A/B testing at the block level, and it felt downright magical once it clicked. If you’re curious, you can peek at its implementation here: inlined-fns.ts.
Have you run into hydration mismatches or found other clever solutions? I’d love to hear about your experience. Feel free to reach out, and if you’d like to see what else I’m up to, visit my portfolio at sidharthmohanty.com.
Top comments (0)