Using window in React SSR: The Complete Guide What “window is not defined” and “expected server html to contain div” really mean
Posted on by Stephen CookYou add Server-Side Rendering (SSR) to your React app and a terrifying “uncaught reference error” greets you:
window is not defined
If you side-step that issue, this equally unhelpful warning raises its head:
Expected server HTML to contain a div
Uhh… What’s going on?
Why is window
not defined?
With SSR, your app runs twice. In the end, it runs on the user’s browser, business as usual. But first, it runs on a server.
What you need to remember is that on a server, things like window.innerWidth
do not make sense. What’s the width of the browser window? There is no browser window. Because of this, servers don’t provide a window
global.
The same goes for document
and the document is not defined
error, as well as some other browser globals.
Sometimes, it’s not even your code that depends on window
. At the time of writing this, hellosign-embedded, react-stripe-elements, and react-chartjs all depend on window
and break if you try to render them with SSR.
To get around this, we can check if window
is available before using it, by running something like if (typeof window === "undefined") return null
. This lets us run the same code on the server and the browser.
Hydration Warnings
But wait! Returning null
in your component’s render function when window
isn’t defined is dangerous if you don’t understand what’s going on.
When ReactDOM.hydrate
runs, it builds up the VDOM of your app on the user’s browser and then compares this to the actual DOM (which has been SSR’d with some initial content).
If the VDOM and the DOM don’t match up, then ReactDOM
gets very confused. That is what expected server code to contain div
means; in the VDOM there’s a div, but not in the DOM.
But why do we care? Can’t we suppress or ignore the warning?
Yes, you can suppress the warning with suppressHydrationWarning
— but you shouldn’t. Doing so can seriously break your app.
If the VDOM and DOM don’t match up, then React might ignore an entire part of the VDOM. In other words, if you do something like this:
const MyComponent = () => {
// Careful, this can cause hydration issues and break your app!
if (typeof window === "undefined") return null;
return <div>🍩</div>;
}
Then you might never see your precious 🍩.
You might get away with it, depending on how the React internals happens to render the specific component. But in my experience, you have about a 50/50 chance of something going wrong.
Safely Using window
So how do we safely use window
without causing our app to break?
Fortunately, we can create a small React hook to detect if we’re on the server or not.
import { useState, useEffect } from "react";
const useIsSsr = () => {
// we always start off in "SSR mode", to ensure our initial browser render
// matches the SSR render
const [isSsr, setIsSsr] = useState(true);
useEffect(() => {
// `useEffect` never runs on the server, so we must be on the client if
// we hit this block
setIsSsr(false);
}, []);
return isSsr;
}
Now we can use this hook instead of checking for typeof window
directly.
const MyComponent = () => {
const isSsr = useIsSsr();
if (isSsr) return null;
return <div>🍩</div>;
}
This hook guarantees that our initial browser render matches the initial server render. Then, we immediately render again, filling in the components that need window
, all without confusing ReactDOM.
Preventing Flashing
Great, we’re doing things safely now — but now we see our components that rely on window
pop in, which looks a bit janky.
The key here is to do more than return null
when on the server. Although return null
is super easy, it means that in the initial payload we’re not sending the component’s markup at all. So when we eventually boot up the app fully on the browser, the component suddenly appears — since it wasn’t there at all before.
For some components, this is okay. When it’s not — we need to create a placeholder version of the component.
We don’t need this placeholder component to have any functionality. We only need it to look like the component, without depending on window
.
Let’s take this component, for example:
const WindowSizePredictor = () => {
const isSsr = useIsSsr();
if (isSsr) return null;
const screenWidth = window.innerWidth;
const lookIntoBall =
'🔮 Look into the crystal ball... Yes, I see it clearly...';
const yourWidthIs = `Your window is ${screenWidth}!`;
return (
<div>
<p>{lookIntoBall}</p>
<p>{yourWidthIs}</p>
</div>
);
}
We might prevent the flash by changing it to this:
const WindowSizePredictor = () => {
const isSsr = useIsSsr();
const screenWidth = isSsr ? null : window.innerWidth;
const lookIntoBall = screenWidth
? '🔮 Look into the crystal ball... Yes, I see it clearly...'
: '🔮 Look into the crystal ball...';
const yourWidthIs = screenWidth
? `Your window is ${screenWidth}!`
: 'Hmm...';
return (
<div>
<p>{lookIntoBall}</p>
<p>{yourWidthIs}</p>
</div>
);
}
Summary
window
is not defined on the server, so you can’t use it during the render of a component being SSR’d.
During a component render, use the useIsSsr
hook.
If outside of a component render, then directly check typeof window === "undefined"
to see if you’re on the server or the browser.
You can just if (isSsr) return null
, but if this causes visual flashing, then consider showing a placeholder instead. It’s extra work, but the polish is worth it!