Introduction
In Next.js, you can speed up your app and provide a better user experience by lazy loading components. The official docs for next/dynamic describe it as a “composite of React.lazy() and Suspense.”
However, in practice, it differs in a few important ways—especially when data fetching is involved. Let’s dive in and see how next/dynamic works, why you’d combine it with React Suspense, and what really happens during server and client rendering.
1. next/dynamic in a Nutshell
- Client-Only Execution: Even if your component has 'use client', Next.js still attempts a minimal server render (generating some HTML) unless you explicitly disable SSR with ssr: false. Using next/dynamic with ssr: false forces the component to only render on the client.
- Lazy Loading: It splits the bundle, loading the component only when needed.
- Custom Loading UI: The loading option provides a placeholder while the imported component is being downloaded from the server.
Example:
const LazyLoadedComponent = dynamic(() => import('./MyComponent'), {
ssr: false,
loading: () => <p>Loading component...</p>,
});
In this setup, Next.js will not server-render MyComponent; it sends a minimal placeholder to the browser. Only once the JavaScript for MyComponent is downloaded does it render.
2. next/dynamic vs. React.lazy() and Suspense
The docs mention: “next/dynamic is a composite of React.lazy() and Suspense.”
The reality: While next/dynamic harnesses a similar underlying mechanism to chunk-split components, it does not automatically suspend on data-fetching Promises. Instead:
- If you’re fetching data within a dynamically imported component (e.g., using React Query’s useSuspenseQuery), React Suspense needs to handle that separately from the dynamic import’s loading UI.
Hence, you might still see two different loading states if you’re not careful:
- One for component import (handled by next/dynamic loading).
- Another for data loading (handled by Suspense fallback when your data-fetching hook suspends).
3. Why Use Suspense at All?
React Suspense provides a unified way to handle asynchronous operations, such as data fetching or component loading. If your dynamically imported components also fetch data with Suspense-friendly libraries (like React Query’s useSuspenseQuery
), you can wrap them in a boundary to have a single fallback UI for both component loading and data loading.
'use client';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
import { useClientStore } from '@/providers/client-store-provider';
import PantryBoxesSkeleton from './PantryBoxes/PantryBoxesSkeleton';
// Forcing client-only rendering and a placeholder while the import downloads
const GuestUserPantry = dynamic(() => import('./GuestUserPantry'), {
ssr: false,
loading: PantryBoxesSkeleton,
});
const LogInUserPantry = dynamic(() => import('./LogInUserPantry'), {
ssr: false,
loading: PantryBoxesSkeleton,
});
export default function Pantry() {
// Example: Zustand store check
const isLoggedIn = useClientStore((state) => state.user.isLoggedIn);
// Dynamically pick the right component
const Component = isLoggedIn ? LogInUserPantry : GuestUserPantry;
return (
<Suspense fallback={<PantryBoxesSkeleton />}>
<Component />
</Suspense>
);
}
Here:
-
PantryBoxesSkeleton
is shown while the component is being dynamically imported. - If
GuestUserPantry
orLogInUserPantry
suspends while fetching data, falls back toPantryBoxesSkeleton
again until data resolves. - The result is a single consistent skeleton UI, avoiding multiple flickering loaders.
4. Key Takeaways & Best Practices
Use next/dynamic for True Client-Only Components
Setting ssr: false ensures Next.js doesn’t attempt to render on the server at all, especially handy for code that is purely browser-specific (e.g., accessing window or localStorage).-
Combine next/dynamic with Suspense for Seamless Fallback
- loading in next/dynamic covers the import phase.
- Suspense fallback covers data fetching or any other asynchronous tasks in the child component.
Beware of Double Loading States
If you don’t unify your placeholders, users might see one loading UI for dynamic imports and another loading state for data fetching. Reuse the same fallback wherever possible.Understand That next/dynamic and React.lazy() Differ
Even though the docs liken them, next/dynamic is a Next.js abstraction that doesn’t automatically tie into Suspense’s data-fetching logic. They can work together, but they address different parts of the lazy-loading process.
Conclusion
While Next.js’s next/dynamic is often described as a “composite” of React.lazy() and Suspense, it does not handle data-fetching Promises the way Suspense does. Instead, it’s best to use both: let next/dynamic handle component-level lazy-loading, and let React Suspense handle asynchronous data loading behind the scenes. This dual approach ensures a consistent loading placeholder and a smoother user experience—with minimal flickers and streamlined hydration on the client.
That’s it! By combining next/dynamic (with ssr: false) and React Suspense, you’ll avoid any “double loading” confusion and deliver a polished, high-performance Next.js application. Happy coding!
Top comments (0)