Loading data in NextJS14

Published At: 09/11/2023

In the following blog post we'll showcase how to load data and handle loading states in NextJS for better user experience.

The Problem:

Suppose we are developing a dashboard for our client that displays the amount of users we have in our app and the total revenue the app made thus far.

We might want to fetch the data in two different ways:

  • Two separate data requests (mostly when one request needs to satisfy a certain condition before the next request)
  • In parallel requests using methods such as Promise.all

    // Seperate data requests:

export async function Page() {
  const usersCount = await getUsersCount(); 
  const totalRevenue = await getTotalRevenue();

  return (
    // ...
  )
}

// Parallel data requests: 

export async function Page() {
  const [usersCount, totalRevenue] = await Promise.all([
    getUsersCount(), getTotalRevenue()
  ])
}
  

Let's say the getUsersCount() function takes 1 second to return a response and the getTotalRevenue() function takes 3 seconds to return a response.

Instead of letting the user stare at a blank page for 3 - 4 seconds, with NextJS we can provide a solution for both data fetching techniques.

The Solution:

Two different requests:

Suppose we have a NextJS project with the app router that showcases the dashboard mentioned above in the home route:

    .
└── root folder/
    └── app  /
        └── page.tsx
  
    // page.tsx

export async function Page() {
  const usersCount = await getUsersCount(); 
  const totalRevenue = await getTotalRevenue();

  return (
    // ...
  )
}

  

If we want to fetch the data in two asynchronous calls and only display the data when requests returned a response we can use the special loading.tsx which acts as a UI placeholder when data is fetched:

    .
└── root folder/
    └── app  /
        ├── page.tsx
        └── loading.tsx
  
    // loading.tsx

export function loading() {
  return (
    // A loading UI such as loder, skeleton etc.
  )
}
  

The UI that is returned by the loading component will be visible to the user until all the requests returned responses (in our case for approximately 4 seconds).

Parallel requests:

What happens when we don't actually care for the response of the first request and we want to show the available data immediately?

When we fetch the data with parallel requests and we have a request that is fairly lengthy it could result in the blocking of the entire page component until it loads, which will in our case, result in the users count to display 2 seconds after the data is actually returned.

To fix this issue we can use data streaming.

Data streaming is a technique which allows you to breakdown your component into "chucks" that progressively stream from the server to the client as they are available to display.

We can take advantage of the data streaming technique with React's <Suspense/> component.

Instead of having both requests in the Page component we'll take each request and move it to it's own component:

    // page.tsx

export async function Page() {
  return (
    <>
      <UsersCount/>
      <TotalRevenue/>
    </>
  )
}

// UsersCount.tsx

export async function UsersCount() {
  const usersCount = await getUsersCount();
  return (
    // ...
  )
}

// TotalRevenue.tsx

export async function TotalRevenue() {
  const totalRevenue = await getTotalRevenue();
  return (
    // ...
  )
}
  

Let's assume we know our <TotalRevenue/> component takes significantly more time to load than our <UsersCount/>.

We can import the <Suspense/> component from react and wrap our "slow" component with it:

    // page.tsx

export async function Page() {
  return (
    <>
      <UsersCount/>
      <Suspense>
        <TotalRevenue/>
      </Suspense>
    </>
  )
}

  

Now all the available components will be rendered no matter how long it takes the "slowest" component to load.

But there is still a problem. Right now the <TotalRevenue/> component is hidden until it finally streamed to the client which can cause a wired popping effect and overall not such a good user experience...

To solve that, we'll create and provide a fallback UI element to the <Suspense/> component as the fallback prop:

    // TotalRevenueLoader.tsx

export function TotalRevenueLoader() {
  return (
    // Any elements you want to render until the <TotalRevenue/> component loads...
  )
}



// page.tsx

export async function Page() {
  return (
    <>
      <UsersCount/>
      <Suspense fallback={<TotalRevenueLoader/>}>
        <TotalRevenue/>
      </Suspense>
    </>
  )
}

  

Now when we visit our dashboard we should see the <TotalRevenueLoader/> render immediately, the <UserCount/> components render after 1 second and the <TotalRevenue/> component render after 3 seconds in place of the <TotalRevenueLoader/> component.

Summary:

We learned how to properly load components and data in NextJS with the loading.tsx special file and react's <Suspense/> component for more dynamic and versatile user experiences.

Found any errors? something's not clear? let me know!

Contact me on Linkedin, Github or email me.