StephenCookDev

Using window in React SSR: The Complete Guide What “window is not defined” and “expected server html to contain div” really mean

someone’s hand placed on a rainy window, looking out at other windows Posted on by Stephen Cook

You 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!