Jacob Paris
โ† Back to all content

Solve React hydration errors in Remix/Next apps

Hydration errors affect every server-rendered React app. This is not a bug in Next.js or Remix, but a fundamental issue in the way React's server-side rendering works.

People usually describe hydration errors as mismatches between the HTML that the server generates and what the browser generates, but that's not the whole story.

To explain this, lets look at how React's server-side rendering works. Most implementations follow the same steps.

  1. The server renders the page and serves HTML to the client.
  2. The browser loads the page and React starts running.
  3. React re-renders the page and generates its own HTML.
  4. React then compares the HTML it generated with the HTML the browser is displaying.
  5. If the HTML matches, great! Otherwise, React will throw a hydration error.

Since React is comparing the HTML it generated with the HTML the browser is displaying, anything that changed the HTML between the time the server sent it and the time React started running can cause a hydration error.

What do hydration errors look like?

The main symptom of a hydration error is that React bails on its server-rendered content and does a full client-side render. This can cause the page to flicker and a full refetch of data.

React suppresses errors in production, so these may come up as "Minified React Error #418" or "Minified React Error #425". There are likely more of these but I won't attempt to make a comprehensive list.

In development, you will see a marginally more helpful error message.

  • Text content does not match server-rendered HTML
  • Hydration failed because the initial UI does not match what was rendered on the server.
  • Warning: Expected server HTML to contain a matching <head> in <html>.

In this article, we'll look at some of the most common causes of hydration errors and how to debug them.

๐Ÿ”ฅ Tip: Diff the HTML to find the exact cause

Before we move on to common reasons for hydration errors, one super handy trick is to diff the HTML that the server sends with the HTML that React generates.

There are three places to look for this.

  1. The network tab in your browser's dev tools will show you the HTML that the server sends.
  2. If you remove the <Scripts /> in your root.tsx, Remix will not hydrate and you can use View Source to see the HTML that was present right before hydration.
  3. With the scripts running normally, use View Source to see the HTML that React generated post-hydration.

If you diff these three HTML sources, you can usually find the exact cause of the hydration error.

There are many online HTML diff tools, just google one and paste in the HTML from two of the three sources.

Browser extensions or adblockers

Browser extensions often have permission to modify the live page content of any website, including your app. This can cause hydration errors if the extension modifies the HTML before React has a chance to compare it.

Try your app in an incognito/private browsing window with all extensions disabled. If the error goes away, you know that an extension is causing the problem.

There are also desktop level ad-blockers and security software that may cause similar issues. If you're running one of these, try disabling it or testing on another device. You may be able to add an exception in the tool for your website.

If the extension is only modifying the head of your document and not the body, hydration errors caused by this are a bug in React 18 and have been solved in React 18.3 Canary. Next.js users will have this automatically, but Remix users could try updating to that version or newer to see if it fixes the issue.

Alternatively, Remix users can use remix-island to mount Remix to a specific div in the body, which will prevent changes to the head from causing hydration errors.

Invalid HTML

The browser does a lot of error correction to make sure that pages render reasonably even when the HTML is malformed.

For example, if you illegally nest a <div> inside a <p>, the browser will move the <div> outside of the <p> to make the HTML valid.

Nested forms are prohibited in HTML, so if you have a form inside a form, the browser will move the nested form outside of the parent form to become its sibling instead.

When React hydrates, it will compare the HTML it generated with the HTML the browser is displaying. If the browser moved elements around, React will throw a hydration error.

The solution here is to write React code that outputs valid HTML.

The Remix Dev Tools will now detect at development time whether you're outputting invalid HTML and warn you about it, with a clickable link to the offending line of code in your editor.

Third party scripts or non-react packages

Third party scripts may also modify the HTML on the page. This is especially common with analytics scripts like HotJar or Google Tag Manager (GTM). If you're using a third party script, try removing it and see if the error goes away.

If this is the issue, you may be able to run the script after React has finished hydrating. For example, you can use useEffect to run the script after the first render, or use the Remix Utils ExternalScripts utility to do this for you.

The next/script package is the canonical solution for Next.js.

CSS in JS libraries

CSS in JS is a methodology for styling React apps that defines styles as a side effect of rendering.

In order to get a stylesheet that contains all the styles for a page, React needs to fully render the page to see which components are used and what styles they need. If it tried to stream the HTML, the browser would start displaying the page before React had a chance to generate the stylesheet.

Chakra, Emotion, and Material UI are popular CSS in JS libraries that have this issue.

A common solution is to double-render the page on the server. After the first render, it can extract all the styles it needs, and then the second render can be streamed to the browser.

If you aren't committed to CSS in JS, alternative styling solutions like Tailwind or Vanilla Extract work much better with server rendering and will not cause hydration issues.

Character encoding

Character encoding issues usually present obvious error messages, so they're easy to diagnose.

txt
[Error] Warning: Text content did not match.
Server: "รขโ‚ฌ"
Client: "โ€™"

This happens when you have a mismatch between the character encoding of the server and the client. The most common cause is that the server is sending UTF-8 encoded text, but the client is interpreting it as ISO-8859-1.

To fix this, add the following meta tag to the <head> of your HTML.

xml
<meta
http-equiv="Content-Type"
content="text/html;charset=utf-8"
/>

Timezone mismatches

Likely the most notorious culprit is the Date object. Dates are complicated, and the less you have to deal with them the happier your life will be.

When javascript tries to create a date, it will use the timezone of the machine it's running on. This means that if you create a date on the server and send it to the client, the client will create a date in its own timezone, which will be different from the server's timezone.

If you're trying to render a date, you'll get these hydration issues close to midnight every day when the server date becomes different than the client date.

Non-idempotent functions like UUIDs

If you're using a non-idempotent function like uuid to generate a unique ID, you may get hydration errors. This is because the server and client will generate different IDs.

You can either generate the ID on the server and send to the client, or pass a seed to the function so that the server and client generate the same ID.

If the random value you're trying to generate is an ID or key for a specific component, React 18 includes a useId hook.

Rendering based on client data

Some data only exists on the client and will not be available when the server renders the page for the first time.

For example, you may have an input that takes a default value from local storage. If you render the input empty on the server and then populate it on the client, you'll get a hydration error and an unsightly flash of empty input.

There is no way for the server to know what the client wants to render, so your solution is to find a way to hide the element on the server and during the initial hydration render.

The useHydrated hook from Remix Utils will give you an isHydrated boolean that you can use to conditionally render the element.

This is the easiest solution but has its own caveats

  • It still creates a flash of empty content, though you can use a fade effect to make it less jarring.
  • The elements will not appear at all if javascript fails to load, breaking the progressive enhancement story.

My preferred solution is to use a ProgressiveClientOnly component that will hide the server rendered content with CSS and swap it for the client content if Javascript is available, else it will show the server content.

Professional headshot
Moulton
Moulton

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter

About once per month, I send an email with:

  • New guides and tutorials
  • Upcoming talks, meetups, and events
  • Cool new libraries and packages
  • What's new in the latest versions of Remix

Stay up to date with everything in the Remix community by entering your email below.

Unsubscribe at any time.