DEV Community

Cover image for Perfecting the Loading Experience with next/dynamic and React Suspense
Maria Kim
Maria Kim

Posted on

Perfecting the Loading Experience with next/dynamic and React Suspense

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>,
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. One for component import (handled by next/dynamic loading).
  2. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • PantryBoxesSkeleton is shown while the component is being dynamically imported.
  • If GuestUserPantry or LogInUserPantry suspends while fetching data, falls back to PantryBoxesSkeleton again until data resolves.
  • The result is a single consistent skeleton UI, avoiding multiple flickering loaders.

4. Key Takeaways & Best Practices

  1. 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).

  2. 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.
  3. 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.

  4. 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!

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

👋 Kindness is contagious

Please consider leaving a ❤️ or a friendly comment if you found this post helpful!

Okay