Note that while the hydration mechanism itself is part of React, the strategy of exactly when and how hydration occurs is handled by your framework. In our case, we're using Next.js, so keep this in mind that there may be implementation nuances when applying this knowledge to different frameworks.
Before RSC, it would be typical to show the user a "Skeleton" loading UI, which is a static HTML skeleton of the component but without any interactivity or real data.

This is better than nothing, but hardly the best user experience and is only slightly better than a loading spinner or blank page.
With RSC, it is trivial to display a non-interactive but fully-populated skeleton of a Client Component on the initial page request body, while JS is still loading. Once the page has loaded and is ready to be interacted with, the Client Component is hydrated seamlessly without any visible changes or "popping" effect.
I call this "perfect hydration".
Above is a Client Component with interactivity. To get this on your screen and clickable, here's what happens:
- Even though it's a "client component", it is pre-rendered on the server during the build step (!)
- The output of this rendering, static HTML, is like a "perfect skeleton" of the component
- This skeleton HTML is sent to the client on initial page load, and is shown to the user while the client JS environment is loading
- The skeleton looks identical to the hydrated component's initial state, so it feels like the component is immediately loaded without any "popping" effect
- Once the JS code is downloaded and the client is ready to go, the client "hydrates" the static HTML, adding event listeners and making the component interactive
- Ideally, this hydration step transitions from a "real skeleton" to a "real component" with identical DOM elements, so nobody is the wiser!
But wait, I thought we couldn't use useState
, etc, with Server Components? This is still true for actual Server Components, but Next.js will render Client Components on the server, just so we can do this special hydration trick.
As a reminder, only client components can be interactive, so only client components need to be hydrated:
Hydration works wonderfully as long as the pre-rendered skeleton HTML matches the initially rendered DOM of the hydrated client component. Unfortunately, this is not always the case.


Notable causes of hydration errors include:
- Incorrect nesting of HTML tags
- Using checks like
typeof window !== 'undefined'
- Using browser-only APIs like
window
orlocalStorage
- Using time-dependent APIs such as the
Date()
constructor - Browser extensions or CDNs modifying the page's static HTML
Take, for example, the following simple client component. This would be no trouble before RSC, but it causes a hydration error in Next.js.
Refresh the page to see hydration happening!
This causes a hydration error because the date on the server doesn't match what is rendered on the client. Uh oh!
Luckily, there are a few ways to work around this. One (more elegant) way is to add a placeholder that is replaced when the client is loaded, using useEffect
and useState
.
Another catch-all solution is to completely avoid rendering until we are sure we are only in the browser, and either showing a spinner or a blank space during pre-rendering. This technique is needed for importing some pre-RSC client-only libraries.
You can also avoid hydration errors by skipping the SSR step when you dynamically import a client component. This is particularly useful for lazy-loading big-ass libraries.