When your blog renders in the wrong spot

Everything looked fine in development, but in production the layout was off. The Elements tab showed the truth: a component had mounted in the wrong place. The React Devtools Components tab, however, insisted all was well. Something was lying.

I had misunderstood how React behaves in a server-side-rendering context. And it turns out this misunderstanding is widespread — with real consequences.

The root of the problem

Here’s the kind of code that produces such rendering chaos:

function Navigation() {
  if (typeof window === 'undefined') {
    return null;
  }

  // Pretend that this function exists,
  // and returns either a user object or `null`.
  const user = getUser();

  if (user) {
    return (
      <AuthenticatedNav
        user={user}
      />
    );
  }

  return (
    <nav>
      <a href="/login">Login</a>
    </nav>
  );
};

For a long time I would have considered this perfectly fine. To see why it isn’t, we need to look at how Gatsby and Next.js actually work under the hood.

From blank page to pre-rendered HTML

With traditional client-side React (through tools like create-react-app), the browser receives a nearly empty HTML document plus a few scripts:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- Maybe some stuff here -->
  </head>

  <body>
    <div id="root"></div>
    <script
      src="/static/bundle.js"
    ></script>
    <script
      src="/static/0.chunk.js"
    ></script>
    <script
      src="/static/main.chunk.js"
    ></script>
  </body>
</html>

The browser runs those scripts, React builds the page, and the user stares at a white screen in the meantime. Enter server-side rendering (SSR): frameworks can generate the full HTML on the server so the user sees content immediately, while JavaScript still downloads in the background.

Gatsby and certain configurations of Next.js take this even further. They build the HTML at compile-time, generating one static HTML file per route. When a user hits your site, that file is ready to serve instantly.

The catch? Interactivity. Your static HTML still needs React to manage dynamic behavior on the client. React mounts the same components, builds up a picture of the DOM, and then “adopts” it — a process known as hydration.

Hydration is not a re-render. During hydration React assumes nothing will change; it isn’t hunting for differences. It’s simply attaching event listeners to existing DOM nodes.

When the server and client disagree

The problematic component above is meant for a navigation bar, with three possible outcomes:

  • Logged in → render <AuthenticatedNav>
  • Not logged in → render <UnauthenticatedNav>
  • Unknown state → render nothing

But here’s the conundrum. The HTML was built at compile-time for every user who might visit. At that moment we don’t know who’s logged in, and the initial interactions happen while the user waits for the JavaScript to hydrate — a substantial window on slow networks.

Many sites default to the “logged out” state during that time, causing a flicker you’ve probably seen on sites like The Guardian or Airbnb. The component in question attempted a fix:

const Navigation = () => {
  if (typeof window === 'undefined') {
    return null;
  }

The idea seems sound. The compiles server-side happens in Node.js, where window doesn’t exist. By rendering nothing on the server, we show the correct dynamic UI on the client. The catch: this violates React’s hydration rules.

Gatsby makes it worse

React can sometimes tolerate a hydration mismatch, but the process is optimized for speed, not reconciliation. And while React does issue console warnings for these mismatches, Gatsby opts out of server-side rendering during development. Since that’s precisely when warnings fire, you might never see the problem — until production.

The Gatsby team prioritizes fast iteration over accuracy. There are open issues pushing for a change, but until then, Gatsby devs need to be especially careful with hydration-sensitive code.

The two-pass approach

To avoid these mismatches, we need to make sure the app hydrates cleanly and then kicks off dynamic content. The standard solution:

function Navigation() {
  const [hasMounted, setHasMounted] = React.useState(false);

  React.useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) {
    return null;
  }

  const user = getUser();

  if (user) {
    return (
      <AuthenticatedNav
        user={user}
      />
    );
  }

  return (
    <nav>
      <a href="/login">Login</a>
    </nav>
  );
};

The logic is straightforward. We initialize a state flag, hasMounted, to false. While it’s false, we render nothing (or a placeholder). On mount, the first useEffect fires, and with it we set hasMounted to true, triggering a complete re-render.

The key difference: useEffect only fires after the component has mounted, which means it never runs during the initial hydration.

First pass (hydration): call hook, but hasMounted defaults to false; we render the placeholder, matching the static HTML from compile-time. Second pass (first render after hydration): the flag is now true, so React stamps the real content over the placeholder.

Performance trade-offs

This force-render right after mount generally delays time-to-interactive, which is normally frowned upon. For most apps the dynamic content is small and reconciles quickly, so the performance hit is minimal. If huge sections of your site require personalization, pre-rendering benefits will shrink — but so be it. Dynamic sections can’t be generated ahead of time by definition.

Your miles may vary, so do your own measurement if performance is a concern.

Useful abstractions

If many parts of your site need deferred rendering, repeating this hasMounted pattern each time is tedious. Building it into a small utility component saves sanity:

function ClientOnly({ children, ...delegated }) {
  const [hasMounted, setHasMounted] = React.useState(false);

  React.useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) {
    return null;
  }

  return (
    <div {...delegated}>
      {children}
    </div>
  );
}

Then you can wrap anything that needs the two-pass treatment:

<ClientOnly>
  <Navigation />
</ClientOnly>

Or make it a reusable hook:

function useHasMounted() {
  const [hasMounted, setHasMounted] = React.useState(false);

  React.useEffect(() => {
    setHasMounted(true);
  }, []);

  return hasMounted;
}
function Navigation() {
  const hasMounted = useHasMounted();

  if (!hasMounted) {
    return null;
  }

  const user = getUser();

  if (user) {
    return (
      <AuthenticatedNav
        user={user}
      />
    );
  }

  return (
    <nav>
      <a href="/login">Login</a>
    </nav>
  );
};

That new abstraction solved my production rendering disaster — and kept the codebase sane along the way.

Two-Pass Rendering as a Practical Mindset

The core takeaway from working with server-rendered React frameworks isn't the specific abstraction layers—it's the underlying way you need to reason about the rendering process. Adopting a two-pass mental model clarifies most of the confusion around hydration.

The first pass occurs at build time, well before any user request. This pass establishes the static foundation of the page, covering content that is identical for every visitor. The second pass happens much later in the browser, filling in the dynamic parts that depend on individual user state or client-side effects. Keeping these two phases conceptually separate is a practical approach for debugging and architecting pages.

With over a decade of React development experience, this model has proven indispensable. A deep intuition for how React behaves is often what separates smooth debugging sessions from prolonged battles with framework quirks, such as hydration mismatches. This frustration is precisely what a dedicated learning resource aims to address.

The primary goal of any comprehensive React education should be to build this kind of intuition. The aim is to reduce the time spent stuck on framework oddities and increase the time spent productively building features, ultimately helping developers find more enjoyment in the React ecosystem.