How to Fix: Hydration error when using Turbopack with dynamically imported named exports.

5 min read

Hydration error with Turbopack and dynamically imported named exports in Next.js: cause, fix, and safe patterns

This bug appears when Turbopack evaluates a dynamic import of a named export differently between server and client, producing markup that does not match during hydration. The result is a client-side hydration error even though the same pattern may appear to work with webpack.

If your component is loaded with next/dynamic and you select a named export with .then(mod => mod.NamedComponent), Turbopack can currently generate inconsistent behavior during the server render and client hydration phases. The safest workaround is to avoid dynamically importing a named export directly and instead expose the target component as a default export through a wrapper module.

Understanding the Root Cause

In Next.js, hydration succeeds only when the HTML generated on the server matches what React expects on the client. With Turbopack, the module graph and chunk resolution for named export selection inside dynamic imports can differ from the behavior of webpack-based builds.

A pattern like this is the usual trigger:

const Widget = dynamic(() => import('./Widget').then((mod) => mod.Widget))

Technically, several moving parts are involved:

  • Dynamic module evaluation: next/dynamic expects a component-producing module to be loaded predictably.
  • Named export extraction: using .then(...) introduces an additional transformation step after the import resolves.
  • Server/client parity: if the server render resolves one shape of the module and the client resolves another timing or chunk boundary, React hydrates against mismatched output.
  • Turbopack edge behavior: this issue is specific to the current Turbopack handling of that import pattern, not to hydration in general.

In practice, the server may render fallback or resolved content differently from what the client receives once the named export is extracted. That mismatch triggers the hydration warning or failure.

Step-by-Step Solution

The most reliable fix is to create a small wrapper file that re-exports the named component as a default export, then dynamically import that wrapper instead of selecting the named export inline.

1. Identify the problematic import

If you currently have code similar to this, it is the likely source of the error:

import dynamic from 'next/dynamic'

const NamedWidget = dynamic(() =>
  import('./components/Widget').then((mod) => mod.NamedWidget)
)

export default function Page() {
  return <NamedWidget />
}

2. Create a wrapper module with a default export

Create a new file next to the original component, for example components/NamedWidget.dynamic.tsx:

export { NamedWidget as default } from './Widget'

This removes the inline named-export extraction from the dynamic import path and gives Turbopack a simpler, more stable module boundary.

3. Update the dynamic import to target the wrapper

import dynamic from 'next/dynamic'

const NamedWidget = dynamic(() => import('./components/NamedWidget.dynamic'))

export default function Page() {
  return <NamedWidget />
}

4. If needed, disable SSR for purely client-only components

If the imported component depends on browser-only APIs such as window, document, or layout observers, also disable server-side rendering:

import dynamic from 'next/dynamic'

const NamedWidget = dynamic(() => import('./components/NamedWidget.dynamic'), {
  ssr: false,
  loading: () => <p>Loading...</p>,
})

Use this only when the component is genuinely client-only. It avoids hydration mismatch by skipping the server render for that component.

5. Keep the server and client output deterministic

Even after fixing the import, ensure the component does not render non-deterministic content during the initial pass.

export function NamedWidget() {
  return <div>Stable content</div>
}

Avoid this during first render:

export function NamedWidget() {
  return <div>{Date.now()}</div>
}

6. Restart the dev server after refactoring

Because this issue involves the module graph, restart the Next.js dev process after changing the import structure. Then validate with Turbopack again.

rm -rf .next
next dev --turbo

If you use npm:

rm -rf .next
npm run dev -- --turbo

Use this structure consistently when dynamically importing a component that originally exists as a named export:

// components/Chart.tsx
export function Chart() {
  return <section>Chart</section>
}

// components/Chart.dynamic.tsx
export { Chart as default } from './Chart'

// app/page.tsx or pages/index.tsx
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('../components/Chart.dynamic'))

export default function Page() {
  return <Chart />
}

Common Edge Cases

  • Client component boundaries in the App Router: if the dynamically imported component uses hooks or browser APIs, make sure the file has 'use client' where required. A missing client boundary can look similar to a hydration problem.
  • Fallback content mismatch: if you provide a custom loading component, ensure it renders consistently and does not depend on client-only state during the server pass.
  • Non-deterministic rendering: values from Date.now(), Math.random(), locale-sensitive formatting, or browser-only data can still cause hydration issues even after fixing the import strategy.
  • Mixed export patterns: avoid changing between default and named exports inconsistently across files. Keep one stable dynamic-import wrapper per component.
  • SSR disabled unnecessarily: using ssr: false can hide the symptom, but it is not always the best fix. Prefer the wrapper-module solution first.
  • Dev vs production differences: Turbopack issues can be more visible in development. Always verify whether the behavior also occurs in your production build pipeline.

FAQ

Is this a React hydration bug or a Turbopack bug?

It is best understood as a Turbopack-specific incompatibility with a particular Next.js dynamic import pattern. React reports the hydration mismatch, but the underlying trigger is the named-export dynamic import path.

Why does importing the default export wrapper fix it?

Because it gives next/dynamic a cleaner module target. Instead of importing a module and then extracting a named export in a promise callback, you import a module whose default export is already the component. That reduces ambiguity in chunk and module resolution.

Should I replace all named exports in my codebase?

No. You only need to change the pattern for components that are dynamically imported and trigger the hydration issue under Turbopack. Regular named exports are still fine in many other cases.

For teams adopting Turbopack today, the safest rule is simple: when using next/dynamic, dynamically import a module with a default-exported component. If your real component is a named export, add a tiny wrapper file and import that wrapper instead. It is a small structural change that removes the hydration mismatch and keeps your Next.js code predictable.

For reference and reproduction details, see the GitHub reproduction repository.

Leave a Reply

Your email address will not be published. Required fields are marked *