Editor’s note: This article was reviewed for accuracy by David Omotayo on 19 June 2024 and updated to include recommendations around optimizing loading components, dealing with common issues, incorporating error boundaries, integrating Suspense with third-party libraries, and more.
The Next.js 13 launch in October 2022 teased upcoming support for Suspense, a React feature that lets you delay displaying a component until the children have finished loading. Production-ready support for React Suspense landed in Next.js 13.4 when the App Router was stabilized.
Since then, Next.js has introduced continual updates and improvements to its React Suspense support. Using Suspense in a Next.js project can enhance both DX and app performance, making this concept crucial to understand. In this tutorial, we’ll learn how to create a loading component using Next.js and React Suspense.
Loading screens are a crucial part of any website — by letting the user know that some processing is happening, you can reduce their frustration and decrease the chance that they’ll leave the website. Additionally, by allowing content to load asynchronously, you can improve a website’s UX and performance.
Let’s get started!
React’s Suspense component was first added to React in v16.6, which was released in 2018. Suspense handles asynchronous operations like code splitting and data fetching. In simple terms, it lets you display a fallback component until the child component is fully loaded.
The code below shows React Suspense’s syntax:
<Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>
To load multiple components, you can add multiple child components within the <Suspense>
component. Suspense improves a website’s performance and user experience, becoming an important part of the React ecosystem.
The Next.js App Router introduced a new file convention: you can now add all the files and components related to the route in a single directory. This includes both components and CSS files, so there is no need to create a separate directory for CSS files.
In the route
directory, you can include the loading.js
file to add your loading UI for React Suspense’s fallback component:
Next.js supports server-side rendering, so the UI will take some time to load. In such cases, you can use Suspense to load the UI. The component in loading.js
is defined as a functional component that can be exported as the default. The syntax is below:
export default function Loading() { // You can add any UI inside Loading, including a Skeleton. return <LoadingSkeleton /> }
The Next.js team has worked to keep support for React Suspense up-to-date and efficient, as well as to enhance the DX of using Suspense with Next. Some of the key improvements include:
Now, let’s implement Suspense by building a small project in Next.js.
We’ll build a web application that uses the TMDB API to fetch trending movies. We have two routes:
root(/)
: Displays a welcome screen in the applicationmovies(/movies)
: Displays the trending movies fetched from the APIYou can install Next.js with the command below. Keep in mind that you’ll need to have Node.js pre-installed on your machine:
npx create-next-app@latest --experimental-app
Entering the command above in the terminal will prompt you to answer the following questions:
What is your project named?
: Name your project whatever you wantWould you like to use TypeScript with this project?
: No. If you wish, then you can continue with TypeScript, but there won’t be many changesWould you like to use ESLint with this project?
: Yes — ESLint will be helpful in debugging errorsWould you like to use src/ directory with this project?
: No, we’ll use the latest app
directoryWith that, the project will be automatically set up for you with the necessary packages installed.
There isn’t a lot of boilerplate code in Next.js, but we should clean it up anyways.
Open the app
directory and remove the API
directory, which is for the server. Remove all the CSS code from the global.css
file in the app
directory.
Now, open page.js
and remove all the code within the return
section. Then, enter the following code in page.js
to display a basic page with a welcome message for the user:
async function Page() { return ( <div> <h3>List of trending Movies & TV</h3> </div> ); } export default Page;
Now, let’s look at the layout section from the layout.js
file:
import { Suspense } from "react"; import Link from "next/link"; import Loading from "./loading"; export const metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children }) { return ( <html lang="en"> <body> <h1>Trending Movies & Movies</h1> <li> <Link href="/">Home</Link> </li> <li> <Link href="/movies">Movie</Link> </li> <Suspense fallback={<Loading />}>{children}</Suspense> </body> </html> ); }
In the code above, we created a navbar
in the body section that will be displayed across all the routes. In navbar
, we have links to root
, movies
, and TV
routes.
loading.js
fileIn the root
directory, create the loading.js
file with the code below:
export default function Loading() { return <p>Loading Data...</p>; }
We’ve created a basic loading component for display, but you can add a much more sophisticated loading screen, like spinners or a skeleton loading screen. The functional component and file naming convention will remain the same.
movies
routeLet’s use the Next.js App Router to create a route. The process is similar to previous Next.js versions. Create a directory within the app
directory named movies
. Inside movies
, create a file named page.js
with the code below:
async function getMovies() { let res = await fetch( `https://api.themoviedb.org/3/trending/movie/day?api_key=${process.env.NEXT_PUBLIC_TMDB_API}` ); await new Promise((resolve) => setTimeout(resolve, 2000)); return res.json(); } async function Trending() { let { results } = await getMovies(); return ( <div> <h3>Movies</h3> {results && results.map((index) => { return <li>{index.title}</li>; })} </div> ); } export default Trending;
Above, we have a two-component file. getMovies()
fetches data from the API, which is sent to the default functional component with the name Trending
.
You can see that Trending
is a server-side-rendered component. It has an async functional component for promise-based data fetching because Suspense will know that data is being fetched. We’ve also implemented a delay of two seconds to see the loading component properly.
In the Trending
component, we call the getMovies()
function to get the fetched data. In the return, we are mapping the data to display all the trending movies in a list.
You might find it unusual that we haven’t used Suspense yet. Next.js understands when something is loading; if there is a loading.js
file in the route
or even the root
directory, it will display its loading component when loading occurs.
We can add different loading components separately in every route with the addition of the loading.js
file. Let’s check out the following GIF displaying the output:
Optionally, you can also create boundaries with Suspense. Using these, if any component isn’t fully loaded, then the loading component will be displayed. For better performance, this <Suspense>
component can be in the layout and page components.
There are several common issues and pitfalls that can arise when using a Suspense component and integrating error boundaries. Understanding these can aid in debugging and optimizing your code more effectively. Here are some of the common issues and best practices to avoid them.
If a component within a Suspense boundary throws an error and there is no error boundary to catch it at all, the entire component tree can unmount, meaning all components disappear from the UI. To prevent this, always wrap Suspense components with an ErrorBoundary
to handle errors efficiently:
import React, { Suspense } from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyComponent from './MyComponent'; function App() { return ( <ErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> </ErrorBoundary> )}; export default App;
Suspense requires a fallback to display while the component is loading. So, if you don’t provide a fallback prop to Suspense, you’ll get an error. Always provide a fallback prop to ensure smooth loading of components:
<Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense>
Using Suspense and error boundaries without a clear strategy can result in unpredictable behavior and make it hard to trace errors or manage loading states.
To avoid this, plan the way you nest Suspense and error boundaries carefully. Ensure each Suspense component has a fallback, like a loading spinner, and place error boundaries where they can catch errors effectively:
<ErrorBoundary> <Suspense fallback={<div>Loading parent...</div>}> <ParentComponent> <ErrorBoundary> <Suspense fallback={<div>Loading child...</div>}> <ChildComponent /> </Suspense> </ErrorBoundary> </ParentComponent> </Suspense> </ErrorBoundary>
Concurrent rendering is a key feature introduced in React 18 to improve the performance and responsiveness of React applications. It allows React to work on multiple tasks simultaneously and prioritize important updates. This means React can pause or interrupt low-priority rendering processes to handle urgent tasks, like responding to user input.
Since Suspense “suspends” components until their data is ready, it blocks the entire UI. However, with React’s concurrent rendering, Suspense can better manage loading states. Instead of blocking the entire UI, it can selectively suspend parts of the component tree, keeping the rest of the UI interactive.
Loading components are meant to enhance the overall user experience of an application. However, if implemented incorrectly or sub-optimally, these loading components can negatively impact performance. Let’s discuss some best practices for optimizing loading components in Next.
Break your application into smaller chunks that can be loaded on demand rather than loading the entire application at once. You can do this using the Next.js dynamic
function like so:
import dynamic from 'next/dynamic'; const MyComponent = dynamic(() => import('./MyComponent')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> ); }
Ensure fallback components used during loading are lightweight and minimal. Avoid heavy operations or complex structures in fallback components. Instead, use simple and fast-rendering components. In most cases, a single div
is enough:
<Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense>
You can reduce wait times and avoid blocking the main thread by optimizing data fetching with libraries like SWR or React Query. These libraries prevents unnecessary re-fetching of data by caching fetch response. Additionally, implementing lazy loading and pagination for large datasets can further improve performance.
Implementing Suspense at the root level of your application will cause the entire application to suspend. Instead, apply Suspense only to parts of the UI that depend on the data being loaded.
startTransition
APIWith concurrent rendering enabled, you can use the startTransition
API to mark updates as ‘transition updates’, allowing React to give them lower priority. This ensures that these updates do not block or interrupt higher-priority updates.
Memoizing expensive computations caches their results, preventing unnecessary re-renders and improving the performance of components during suspenseful loading.
Static site generation (SSG) pre-renders the contents and static assets for each page of a website at build time. This approach inherently conflicts with waiting for asynchronous data using Suspense, as there is typically no need to load additional content.
However, if your statically generated page needs to display frequently updated data, and the page content changes with every request, consider using client-side rendering or partial pre-rendering instead. This approach populates the page content dynamically. For more information, check out the Next.js docs on data fetching.
Data fetching libraries like SWR offer a powerful and efficient solution for managing remote data fetching, caching, and revalidation in Next.js applications. With just a single line of code, you can simplify the logic of data fetching while providing the benefits of reusable data fetching, caching, and avoiding request duplication:
const { data, error, isLoading } = useSWR('/api/user', fetcher);
While SWR offers fetch request states such as success, loading, and error for gracefully handling boundaries in the traditional way, it also provides the option to use React Suspense, with additional benefits:
const { data } = useSWR('/api/user', fetcher, { suspense: true })
In Suspense mode, the fetch response is always available in the data
variable and is guaranteed to be ready on render without needing to explicitly check if the data is undefined.
However, Suspense itself doesn’t handle errors that may occur during data fetching. If an error occurs during the fetch request, the promise returned by the data fetching function will be rejected.
To handle errors effectively, always nest your Suspense component within an ErrorBoundary
:
import { Suspense } from 'react' import useSWR from 'swr' function MyComponent () { const { data } = useSWR(url, fetcher, { suspense: true }) return <div>hello, {data.title}</div> } function App () { return ( <ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}> <Suspense fallback={<h1>Loading posts...</h1>}> <MyComponent /> </Suspense> </ErrorBoundary> ) }
Integrating Suspense with a library like SWR provides a powerful combination for efficient data fetching and smooth user experiences. By leveraging Suspense’s blocking, SWR’s caching, and error boundary, you can build resilient and responsive UIs that efficiently handle loading states and errors.
Building a loading component with Next.js and React Suspense can significantly improve the user experience of your web application. With Suspense, you can easily create loading states for data-fetching and dynamic imports, making your application more responsive and efficient.
By following the steps outlined in this article, you can create a smooth loading experience for your users, reducing frustration and improving engagement. Whether you’re building a simple website or a complex application, using Next.js and React Suspense can help you create a more seamless and enjoyable user experience.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
One Reply to "Using Next.js with Suspense to create a loading component"
What do you mean by “we haven’t used Suspense yet” when you have this line in layout.js file?
<Suspense fallback={}>{children}