How to Fix: Hydration Error When Using TurboPack
Hydration errors with TurboPack usually mean the HTML generated on the server does not match what React renders in the browser. In a Next.js app using the App Router, this often happens when a component depends on browser-only APIs, non-deterministic values, or client-side behavior without being marked correctly.
Table of Contents
Understanding the Root Cause
In Next.js, pages inside the app directory are treated as Server Components by default. When you create a simple component and import it into app/page.js or app/page.tsx, React first renders markup on the server, then hydrates that markup in the browser.
The error appears when the server-rendered output differs from the client-rendered output. With TurboPack, this mismatch can become more visible during development because its fast incremental builds expose rendering inconsistencies quickly, but TurboPack is usually not the true cause. The actual problem is almost always one of these:
- Using window, document, localStorage, or other browser-only APIs during render.
- Rendering values that change between server and client, such as Date.now(), Math.random(), locale-dependent formatting, or timestamps.
- Using stateful or interactive logic in a component that is still being treated as a Server Component.
- Conditionally rendering different markup based on environment checks like typeof window !== ‘undefined’ directly inside the returned JSX.
- Importing a client-only dependency into a server-rendered component tree.
For example, this can break hydration:
export default function SimpleComponent() {
return <div>{Date.now()}</div>;
}
The server prints one timestamp, and the browser calculates another. React sees different HTML and throws a hydration mismatch.
Another common example:
export default function SimpleComponent() {
const theme = localStorage.getItem('theme');
return <div>{theme}</div>;
}
This fails because localStorage does not exist during server rendering.
Step-by-Step Solution
The fix depends on whether your component should be a Server Component or a Client Component.
1. Mark interactive or browser-dependent components as client components
If your component uses hooks like useState, useEffect, event handlers, or browser APIs, add ‘use client’ at the top of the file.
'use client';
import { useEffect, useState } from 'react';
export default function SimpleComponent() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
}
}, []);
return <div>Current theme: {theme}</div>;
}
Then import it into your page normally:
import SimpleComponent from './SimpleComponent';
export default function Page() {
return (
<main>
<SimpleComponent />
</main>
);
}
2. Move browser-only logic into useEffect
Even in a client component, avoid reading browser APIs during the initial render if they can affect the generated markup. Read them inside useEffect so the first render stays stable.
'use client';
import { useEffect, useState } from 'react';
export default function UserAgentInfo() {
const [userAgent, setUserAgent] = useState('');
useEffect(() => {
setUserAgent(window.navigator.userAgent);
}, []);
return <p>{userAgent || 'Loading browser info...' }</p>;
}
3. Remove non-deterministic values from the initial render
If the server and client produce different values, hydration will fail. Generate those values after mount, or compute them on the server and pass them as props.
Problematic version:
export default function Clock() {
return <div>{new Date().toLocaleTimeString()}</div>;
}
Safer client-side version:
'use client';
import { useEffect, useState } from 'react';
export default function Clock() {
const [time, setTime] = useState('');
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <div>{time || 'Loading time...' }</div>;
}
4. Keep server components pure
If a component does not need interactivity, keep it as a Server Component and make sure it renders deterministic content only.
export default function StaticWelcome() {
return <h1>Welcome to Event Manager</h1>;
}
5. Isolate third-party packages that depend on the browser
Some libraries access the DOM immediately. Wrap them in a client component, or import them dynamically with SSR disabled when appropriate.
'use client';
import dynamic from 'next/dynamic';
const BrowserOnlyWidget = dynamic(() => import('./BrowserOnlyWidget'), {
ssr: false,
});
export default function WidgetSection() {
return <BrowserOnlyWidget />;
}
Use this approach carefully. Disabling SSR is useful for browser-only widgets, but not ideal for content that should be server-rendered for SEO.
6. Restart TurboPack after structural fixes
After changing component boundaries between server and client, stop the dev server and restart it. Development caches can sometimes hold onto stale module state.
npm run dev
# or
pnpm dev
# or
yarn dev
7. Verify your page tree
If app/page.js imports another component, and that component imports a child using hooks or DOM APIs, the child still needs its own ‘use client’ boundary unless the entire import chain is already client-only.
A safe structure looks like this:
app/
page.tsx
components/
SimpleComponent.tsx
BrowserWidget.tsx
// app/page.tsx
import SimpleComponent from '../components/SimpleComponent';
export default function Page() {
return <SimpleComponent />;
}
// components/SimpleComponent.tsx
'use client';
import { useState } from 'react';
export default function SimpleComponent() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Common Edge Cases
- Conditional JSX based on window checks: Writing one branch for the server and another for the client inside render often causes mismatched HTML.
- Locale-sensitive formatting: toLocaleString() and date formatting may produce different output depending on runtime environment.
- Random IDs: Rendering Math.random() or custom generated IDs during SSR can break hydration. Use stable IDs or generate them after mount.
- Third-party UI libraries: Some editors, charts, maps, and drag-and-drop tools assume a browser environment immediately.
- Nested client/server boundaries: A parent page can stay server-rendered while child interactive components are client-rendered, but the boundary must be explicit.
- Strict Mode confusion: In development, React may render more than once, which can make unstable logic look even more broken.
- Environment-specific data: Reading cookies, headers, or request-only values on the server and recomputing something different in the browser can trigger mismatches.
FAQ
Is TurboPack causing the hydration error?
Usually no. TurboPack is exposing a rendering mismatch that already exists. The real fix is to correct the Server Component and Client Component boundary or remove unstable rendering logic.
When should I add ‘use client’?
Add ‘use client’ when a component uses React client hooks, event handlers, browser APIs, or client-only libraries. Do not add it everywhere, because that removes the benefits of server rendering.
Can I fix this by disabling SSR?
Sometimes, but it should be the last option. Using dynamic import with ssr: false works for browser-only widgets, but for regular UI it is better to keep SSR and make the initial render deterministic.
If you are reproducing this issue in the linked project, inspect the imported component first: if it uses hooks, browser APIs, time-based values, or any client-only library, convert it into a proper Client Component and move unstable logic into useEffect. That resolves the vast majority of hydration errors reported when running Next.js with TurboPack.