How to Fix: nextjs 14.2.8 broke dynamic component
Next.js 14.2.8 broke dynamic component loading: why it happens and how to fix it
If your app upgraded to Next.js 14.2.8 and a previously working dynamic component suddenly stopped rendering, the failure is usually not random. In most cases, the upgrade exposed a stricter boundary between Server Components, Client Components, and next/dynamic behavior—especially in the App Router.
Table of Contents
Although the original issue report is minimal, this class of regression typically appears when one of these patterns exists:
- A component imported with next/dynamic now crosses a server/client boundary incorrectly.
- A browser-only dependency is loaded inside a Server Component.
- The dynamic import targets a module export incorrectly after bundling changes.
- ssr: false is used from the wrong place or with the wrong component type.
You can review the framework source and release context through the Next.js repository, but the practical fix is usually in your component structure.
Understanding the Root Cause
In Next.js 14, the App Router heavily relies on React Server Components. That means the framework distinguishes much more aggressively between code that runs on the server and code that runs in the browser.
A dynamic import that worked before can break after an update for technical reasons such as:
- A client-only component is being imported from a Server Component
If the dynamically loaded component uses useState, useEffect, browser APIs like window or document, or third-party UI libraries that assume a browser runtime, it must be inside a file marked with ‘use client’. - The dynamic import resolves the wrong export
If your module uses a named export but the dynamic import expects a default export, bundling changes can make the mismatch fail more consistently. - ssr: false is masking an architectural issue
Developers often use dynamic(() => import(…), { ssr: false }) to force browser-only rendering. That still requires the importing file to be a valid client boundary when the imported code depends on client runtime behavior. - Hydration or module evaluation now happens earlier or more strictly
If a module touches window at import time, not inside an effect, the component can fail before rendering even begins.
The core problem is usually this: the dynamically imported component is not isolated as a true Client Component, or the import pattern no longer matches how Next.js 14.2.8 evaluates modules.
Step-by-Step Solution
The safest fix is to make the client boundary explicit and ensure the dynamic import points to the correct export.
1. Identify whether the broken component is browser-only
If the component uses any of the following, treat it as a Client Component:
- React hooks such as useState, useEffect, useRef
- DOM APIs like window, document, localStorage
- UI libraries that depend on browser rendering
- Event handlers such as onClick
2. Mark the dynamically loaded component as client-only
At the top of the component file, add ‘use client’.
'use client'
Example:
'use client'
import { useEffect, useState } from 'react'
export default function MyWidget() {
const [ready, setReady] = useState(false)
useEffect(() => {
setReady(true)
}, [])
return <div>{ready ? 'Loaded' : 'Loading...'}</div>
}
3. Import it dynamically from a client-safe boundary
If the parent also contains client logic, mark the parent with ‘use client’ too.
'use client'
import dynamic from 'next/dynamic'
const MyWidget = dynamic(() => import('./MyWidget'), {
ssr: false,
loading: () => <p>Loading widget...</p>,
})
export default function PageSection() {
return <MyWidget />
}
This pattern is the most reliable when the component depends on browser APIs.
4. Fix named export vs default export mismatches
If your component is a named export, import it explicitly:
'use client'
import dynamic from 'next/dynamic'
const MyWidget = dynamic(() =>
import('./MyWidget').then((mod) => mod.MyWidget)
)
export default function PageSection() {
return <MyWidget />
}
And the component file should match:
'use client'
export function MyWidget() {
return <div>Named export works</div>
}
5. Move browser globals inside effects or guarded blocks
If your module references window at the top level, it can break during server evaluation.
Broken:
'use client'
const width = window.innerWidth
export default function MyWidget() {
return <div>{width}</div>
}
Fixed:
'use client'
import { useEffect, useState } from 'react'
export default function MyWidget() {
const [width, setWidth] = useState(0)
useEffect(() => {
setWidth(window.innerWidth)
}, [])
return <div>{width}</div>
}
6. If the parent is a Server Component, wrap the dynamic part in a dedicated client wrapper
This is often the cleanest App Router fix.
Client wrapper:
'use client'
import dynamic from 'next/dynamic'
const MyWidget = dynamic(() => import('./MyWidget'), {
ssr: false,
})
export default function MyWidgetClientWrapper(props) {
return <MyWidget {...props} />
}
Server Component parent:
import MyWidgetClientWrapper from './MyWidgetClientWrapper'
export default async function Page() {
const data = await getData()
return <MyWidgetClientWrapper data={data} />
}
This preserves the server-rendered page while isolating the browser-only subtree.
7. Clean and rebuild after changing boundaries
Because dynamic import and RSC graphs are cached aggressively, clear your build artifacts and reinstall if needed.
rm -rf .next
rm -rf node_modules
npm install
npm run build
If you use another package manager:
pnpm install
pnpm build
8. Compare behavior with and without dynamic import
Temporarily replace the dynamic import with a normal import. If the normal import works only in a client component, the issue is almost certainly a server/client boundary problem rather than the component logic itself.
'use client'
import MyWidget from './MyWidget'
export default function PageSection() {
return <MyWidget />
}
If this works, reintroduce next/dynamic carefully.
Common Edge Cases
- Third-party libraries touching the DOM during import
Charts, editors, maps, and animation libraries often access browser APIs before render. They almost always need a client wrapper plus ssr: false. - Using dynamic import inside a Server Component expecting client behavior
A Server Component can render a client child, but the child must be explicitly isolated. Do not assume dynamic import alone makes code client-safe. - Mismatched exports after refactoring
If the file changed from export default to a named export, your old dynamic import may silently become invalid. - Hydration mismatch caused by time, locale, random values, or viewport logic
Even if the component loads, it can still break hydration if server and client output differ at first render. - Transpilation differences in monorepos
If the component lives in a shared package, make sure the package is compatible with your Next.js build setup and client boundary expectations. - Implicit server usage of client hooks
A file without ‘use client’ cannot safely use hooks like useEffect, even if the code is only reached through a dynamic path.
FAQ
Why did this work before Next.js 14.2.8?
Most likely, your code relied on a boundary that was previously tolerated but not structurally correct. The update made module evaluation, RSC handling, or dynamic import resolution stricter, exposing the bug.
Should I always use ssr: false for dynamic components?
No. Use ssr: false only when the component truly requires a browser environment. If the component can render on the server, keep SSR enabled for better performance and SEO.
What is the safest long-term fix?
The safest fix is to separate Server Components and Client Components explicitly, add ‘use client’ where needed, avoid top-level browser globals, and make dynamic imports match the correct export type.
In short, the regression is usually not that next/dynamic is broken globally—it is that Next.js 14.2.8 now exposes invalid client/server boundaries more clearly. Once you isolate the component as a proper client-only subtree and clean up the import shape, dynamic loading becomes stable again.