VOOZH about

URL: https://blog.logrocket.com/react-hydration-pre-rendered-html/

โ‡ฑ A look at React hydration and pre-rendered HTML - LogRocket Blog


2023-10-04
1292
#react
Theodorus Clarence
178870
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

In this article, Iโ€™ll cover the underlying reason behind the Expected server HTML to contain a matching <tag> in <tag> error. This error is caused by React hydration, which is a conversion process for pre-rendered HTML. React hydration is generally used to add interactivity to pre-rendered HTML generated by the server.

๐Ÿ‘ A Look At React Hydration And Pre-Rendered HTML

This article will provide a closer look at React hydration and pre-rendered HTML. We will look at the underlying APIs and how they work, as well as a common hydration error, and how to troubleshoot it.

Jump ahead:

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

The hydrateRoot API

A popular framework like Next.js uses React hydration under the hood. As such, we might not be familiar with it because it is usually pre-configured deep inside the Next.js framework. As you can see in the picture below, Next.js is using a React DOM API called hydrateRoot inside the rendering function:

๐Ÿ‘ Using The HydrateRoot React DOM API

The hydrateRoot API can also be implemented within other frameworks, such as Gatsby and Astro. Although we probably wonโ€™t use the hydrateRoot API by ourselves, it is important to understand how it works so we can handle all the pitfalls and errors that come with it.

Understanding React hydration

React hydration bridges the gap between server-side and client-side rendering. It adds interactivity to static HTML generated on the server, enhancing initial rendering speed and SEO compared to traditional single-page apps.

With React hydration, visitors to your page will be served a static HTML from the server. After it loads, React will โ€œattachโ€ all of the event listeners, such as state, effect, etc., to their respective elements. The hydration API that is provided by React will take that HTML and find the button that you want to attach the onClick listener on:

๐Ÿ‘ The HydrateRoot API Pattern

Hydration with hydrateRoot

To understand why hydration needs to happen, we need to understand how React sees the static HTML that needs to be hydrated.

This is a generated static HTML using the React DOM API called renderToString:

import { renderToString } from 'react-dom/server';

const html = renderToString(<App />);

After running the code, we get something like this:

<h1>Counter App</h1>
<button>
 You clicked me
 <!-- -->0<!-- -->
 times
</button>

When we see this piece of HTML code, it simply renders a heading with text containing Counter App and a button that might be a counter. But, we donโ€™t have the JavaScript listeners for that, so clicking the button wonโ€™t change anything.

If we compare them to the React code, we are adding functionalities such as onClick like so:

export default function App() {
 const [count, setCount] = React.useState(0);

 return (
 <>
 <h1>Counter App</h1>
 <button onClick={() => setCount(prev => prev + 1)}>
 You clicked me {count} times
 </button>
 </>
 );
}

Hydration will be in charge of attaching the onClick event listener using the JavaScript code that is fetched on the client side. It will also be responsible for the entire interactivity of the page, such as the incrementing count.

That hydration happens by using the hydrateRoot API like this:

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document.getElementById('root'), <App />);

After getting the pre-rendered HTML from the document file, the hydrateRoot API will match them with your App component to make sure that there is no mismatch.

Common hydration error in React

Now letโ€™s talk about why this error happens:

๐Ÿ‘ A Common Hydration Error In React

When we are hydrating a server-rendered HTML, React expects the rendered contents to be identical to the server-rendered contents. So when you have a heading that contains App Counter in the App.jsx file, React is going to assume that you have it on your server-rendered HTML.

React will actually treat differences as errors, but the errors wonโ€™t come out in the production. Still, you need to fix them, because they can lead to a slower app, or even worse, the event handlers might get attached to the wrong elements.


Over 200k developers use LogRocket to create better digital experiences

๐Ÿ‘ Image
Learn more โ†’

Itโ€™s important that the server-rendered HTML of the app strictly conforms to a specific structure, like so:

<div id="root"><h1>Counter App</h1><button>You clicked me <!-- -->0<!-- --> times</button></div> 

There canโ€™t be any extra whitespace or new lines.

Reproducing the error

Letโ€™s say that we have an app that renders the current date to the exact millisecond using ISO string date function:

export default function App() {
 return <div>{new Date().toISOString()}</div>;
}

This is the server-rendered HTML that we get from the renderToString API:

<div>2023-08-23T09:14:11.745Z</div>

When we hydrate the server-rendered HTML later on, there is no chance that the date is going to be the same as the server-rendered HTML. This causes the dreaded hydration error:

๐Ÿ‘ A Common Hydration Error In React

Pretty simple right? Hereโ€™s a list of the most common causes leading of hydration errors from the React docs:

  • Extra whitespace (like newlines) around the React-generated HTML inside the root node
  • Using checks like typeof window !== 'undefined' in your rendering logic
  • Using browser-only APIs like window.matchMedia in your rendering logic
  • Rendering different data on the server and the client

Troubleshooting the React hydration error

Suppressing the error

There are two ways to fix the hydration error we saw above. The first way is by using the supressHydrationWarning props:



export default function App() {
 return <div suppressHydrationWarning={true}>{new Date().toISOString()}</div>;
}

This should be used if a single element attribute or text contents will be different no matter the case, like an ISO string date. You can silence the error by setting the supressHydrationWarning props to true.

This solves the error and is a great solution if you want to fix simple text mismatches like this date example.

Using the useEffect Hook

If you have a more complex app that has different contents between the client and server content, youโ€™ll need to use this second solution. The second solution is to set values inside the useEffect Hook instead of directly in the JSX.

For example, you have a logic that differentiates if you want dark mode or light mode based on the hours. If the hours indicate that it is later than 12 AM, we want to show dark mode. Otherwise, we want to show light mode:

export default function App() {
 return (
 <h1>
 {new Date().getHours() > 12 ? "Dark Mode" : "Light Mode"} 
 </h1>
 );
}

You can fix the error by creating a React state called isDark and putting the logic inside the useEffect function. We can then use the isDark state to conditionally render dark mode or light mode:

export default function App() {
 const [isDark, setIsDark] = useState(false);

 useEffect(() => {
 setIsDark(new Date().getHours() > 12);
 }, [])

 return (
 <h1>
 {isDark ? "Dark Mode" : "Light Mode"} 
 </h1>
 );
}

By using this method, the server-rendered HTML will use the default value of useState first, so it matches the data from when we ran hydrateRoot. Then after the app mounts, it will change the contents based on the logic.

Conclusion

Hydration errors are a common occurrence, especially when dealing with elements like pre-rendering an exact ISO string date. While it is possible to suppress warnings to manage these errors, itโ€™s important not to overuse them. To handle different contents between the client and server, you can move the logic after the app is mounted using the useEffect Hook.

Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not server-side

    $ npm i --save logrocket 
    
    // Code:
    
    import LogRocket from 'logrocket'; 
    LogRocket.init('app/id');
     
    // Add to your HTML:
    
    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

๐Ÿ‘ Image
Chizaram Ken
Jun 16, 2026 โ‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ€” no new framework required.

๐Ÿ‘ Image
Ikeh Akinyemi
Jun 12, 2026 โ‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

๐Ÿ‘ Image
Chizaram Ken
Jun 8, 2026 โ‹… 11 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now