next/dynamic Named Imports Cause Hydration Errors: The Real Fix
You added a third-party component with next/dynamic because someone on GitHub said it fixes SSR problems. It compiled. And now, at runtime, React is throwing a hydration error that points at a <div> you did not write, in a library you cannot edit, and the stack trace is a wall of minified names. You suppress it. It comes back on the next deploy. You wrap another component. The whole tree turns into a client component and your page stops being server-rendered at all.
Here is the part nobody tells you: a large fraction of these "hydration" errors are not hydration bugs at all. They are dynamic import bugs wearing a hydration costume. The failure mode looks identical to a stray Date.now() or a browser-only API in render, so you go hunting for a mismatch that is not there. The real problem is how you imported the thing.
When you dynamically import a named export, you must resolve it to a default yourself. dynamic(() => import('pkg')) gives the whole module object, whose default is undefined, and React renders nothing on the server and something on the client, which reports as a hydration mismatch. Do this instead:
const Chart = dynamic(
() => import('some-lib').then((mod) => mod.Chart),
{ ssr: false, loading: () => <div className="h-64" /> }
);
And in the App Router, ssr: false only works inside a file marked 'use client'. Put the dynamic import in a small client wrapper, not in a Server Component.
The error you are actually seeing
It arrives in one of these two shapes, depending on your Next.js version:
Hydration failed because the server rendered HTML didn't match the client.
As a result this tree will be regenerated on the client.
Warning: Expected server HTML to contain a matching <div> in <div>.
Sometimes it is even blunter, and this one is the tell that you have an import problem rather than a render problem:
Element type is invalid: expected a string (for built-in components)
or a class/function (for composite components) but got: undefined.
That got: undefined is the smoking gun. React was handed undefined where a component should be, because your dynamic import resolved to a module object whose default property does not exist.
Why a named import breaks and a default import does not
next/dynamic expects the factory you pass it to resolve to a module with a default export that is the component. When you write dynamic(() => import('react-chartjs-2')) against a package that has no default export, the promise resolves to the whole namespace object { Chart, Line, Bar, ... }. Next.js reads .default off that object, finds undefined, and hands undefined to React as the component to render.
On the server pass, rendering undefined produces nothing, or a placeholder. On the client pass, the loader may resolve differently, or the fallback swaps in at a different moment, and now the server HTML and the client's first render disagree. React compares them, sees a structural difference, and reports a hydration mismatch. The word "hydration" in the error sends you looking for non-deterministic render logic, but the cause is upstream: the component was never correctly resolved in the first place. This is the same trap as a generic Next.js hydration mismatch, except the mismatch is manufactured by the import, not by your JSX.
The reason it is so easy to get wrong is that the correct-looking syntax works for default exports and fails silently for named ones. Both compile. Both type-check. Only one renders.
The fix, step by step
Step 1: Resolve the named export inside the factory
Add the .then() that plucks the named export out of the module. This is the actual fix for the got: undefined class of error.
// Before: resolves to the module object, default is undefined
const Chart = dynamic(() => import('react-chartjs-2'));
// After: resolves to the component itself
const Chart = dynamic(() =>
import('react-chartjs-2').then((mod) => mod.Chart)
);
After this change, the Element type is invalid ... got: undefined error disappears, because React now receives a real component function.
Step 2: Decide ssr true or false on purpose, not by default
This is where most people flail, wrapping everything in ssr: false as a superstition. Choose deliberately:
| Setting | Use when | Cost |
|---|---|---|
ssr: true (default) | The component renders identical output on server and client (pure, deterministic) | None. Keep it. You get server HTML and SEO. |
ssr: false | The component touches window, document, localStorage, canvas, or measures the DOM. Charts, maps, rich editors, anything that reads layout. | No server HTML for that subtree. It is blank until JS loads, so give it a fallback of the right size. |
The rule: disable SSR for the specific component that cannot render on the server, never for the whole page. If a charting library reaches for window during render, ssr: false on that one chart is correct. Turning the parent route into a client component to silence it is throwing away server rendering for the entire page to fix one widget.
Step 3: Give it a stable, correctly sized loading placeholder
A hydration mismatch can also come from the fallback being a different size or shape than the loaded component, causing a layout shift that React notices. Reserve the space:
const Chart = dynamic(
() => import('react-chartjs-2').then((mod) => mod.Chart),
{
ssr: false,
loading: () => <div className="h-64 w-full animate-pulse rounded" />,
}
);
The placeholder should occupy the same box the real component will. This also removes the visible content jump, which is a UX win on top of the correctness win.
Step 4 (App Router only): move the dynamic import into a client wrapper
In the App Router, ssr: false is not allowed in a Server Component and current Next.js will error at build with a message telling you exactly that. The fix is a tiny client component whose only job is to own the dynamic import:
// components/chart-client.tsx
'use client';
import dynamic from 'next/dynamic';
const Chart = dynamic(
() => import('react-chartjs-2').then((mod) => mod.Chart),
{ ssr: false, loading: () => <div className="h-64 w-full" /> }
);
export default function ChartClient(props) {
return <Chart {...props} />;
}
Then your Server Component imports ChartClient like any other component. The rest of the page stays server-rendered. Only the client wrapper and the chart ship to the browser.
Verification
You want three things to be true. First, the console error is gone on a hard reload with the cache disabled, not just on a warm navigation. Second, view source (Ctrl+U) or curl the page and confirm the rest of your content is present in the server HTML while the dynamic component's slot shows your placeholder markup. That proves you disabled SSR for one component, not the whole tree.
curl -s https://your-site.example/dashboard | grep -c 'h-64' # placeholder is in the SSR output
curl -s https://your-site.example/dashboard | grep -c 'Your page heading' # real content still SSR'd
Third, in the browser Network tab, confirm the library's chunk loads as a separate file after the initial HTML. That is the code-splitting you wanted, and it confirms the dynamic boundary is where you put it.
What people get wrong
Reaching for suppressHydrationWarning. This does not fix the mismatch, it hides the symptom and freezes the server's value for that element. As covered in why suppressHydrationWarning is not cosmetic, it is a scalpel for one intentionally-dynamic node (a timestamp), not a bandage for a broken import. On a component that resolved to undefined, it does nothing useful.
Making the whole page a client component. Adding 'use client' to the route to stop the error works, in the sense that a fire stops burning once the house is gone. You lose server rendering, streaming and the RSC payload benefits for everything on that page to fix one widget. Isolate the client boundary to the component that needs it.
Assuming it is your render logic. People spend hours auditing their JSX for Date, Math.random and typeof window when the culprit is a missing .then((mod) => mod.Named). If the error mentions got: undefined, or it appeared the moment you added a next/dynamic import, check the import before you audit the render.
When it is still broken
If the resolve, the client wrapper and the placeholder are all correct and you still get a mismatch, check three things. First, if you are on Turbopack, there is a separate known dynamic-import hydration issue (tracked as vercel/next.js #70795) where Turbopack and Webpack resolve dynamic modules differently; try a production build with Webpack to confirm whether the bug is yours or the bundler's, and see hydration mismatches that are not your fault. Second, if the library itself has module-level side effects (it reads window at import time, not render time), ssr: false is mandatory because even importing it on the server throws; the error there is often a ReferenceError: window is not defined during the build, not a hydration warning. Third, if the component renders fine but flashes, your placeholder is the wrong size, so match its height and width to the loaded component and the layout shift stops.
Frequently asked questions
- Why does next/dynamic return undefined for a named export?
- next/dynamic reads the default export off the resolved module. If the package only has named exports, the default is undefined, so React is handed undefined as the component and throws 'Element type is invalid ... got: undefined', which then cascades into a hydration mismatch. Resolve the named export yourself with import('pkg').then((mod) => mod.Name).
- Can I use ssr: false in the App Router?
- Only inside a Client Component. next/dynamic with ssr: false is not allowed in a Server Component and current Next.js errors at build time. Put the dynamic import in a small file marked 'use client' that exports a wrapper, then import that wrapper from your Server Component so the rest of the page stays server-rendered.
- Should I add suppressHydrationWarning to fix this?
- No. suppressHydrationWarning freezes the server value for one element and is meant for intentionally dynamic nodes like timestamps. It does not fix a component that resolved to undefined or a bad dynamic import; it only hides the warning while the component still renders wrong. Fix the import instead.
- When should I disable SSR for a dynamically imported component?
- When the component touches browser-only APIs during render: window, document, localStorage, canvas, or anything that measures the DOM (charts, maps, rich text editors). Disable SSR for that specific component only, never for the whole page, and give it a loading placeholder sized like the real component to avoid layout shift.