How to handle loading screens in Next JS
Tags: nextjs, react, typescript, javascript
Published: 2023-11-18 06:16:01

In this post, we'll cover how to handle loading screens in Next JS.

**note: I'm using Next JS 13, so this may or may not be compatible for lower versions.**

Our loading Page will look like this


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u16nfbbagfjozpt3mkz.png)

To achieve this in Next JS, we have to make use of react [suspense ](https://react.dev/reference/react/Suspense).

So first we must declare a file name loading.tsx (or .jsx if you're not using typescript). Yes, the filename must be named **loading**.

refer to [this](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming).

```typescript
"use client";

export default function Loading() {
  return <>Loading...</>;
}
```

It will be placed along our other next related files, such as layout, page, etc.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hck5c7xew00lzebatztf.png)

Our flow is a bit different. The actual loading.tsx will be used between navigating different nav links, this is due to our nested layout structure. 

We need a loading page for the initial load on our page.

So lets take a look at the UI first, to get an idea on what the code may look like.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/edtagbxb6ama1c76a5f6.png)



Being that we want the loading page to act as an overlay, we have to make some adjustments to the layout.tsx.

**layout.tsx**

```typescript
"use client";

import dynamic from "next/dynamic";
import "../index.css";
import React, { Suspense } from "react";
import AuthGuardWrapper from "../../src/common/AuthGuardWrapper";

import LoadingPage from "../../src/common/LoadingPage";

const DefaultPageWrapper = dynamic(
  () => import("../../src/common/DefaultPageWrapper"),
  { ssr: false }
);

const Settings = dynamic(() => import("../../src/Pages/Settings"), {
  ssr: true,
  loading: () => <LoadingPage />,
});

const Nav = dynamic(() => import("../../src/common/Nav"), { ssr: false });

const PageView = dynamic(() => import("../../src/common/PageView"), {
  ssr: true,
  loading: () => <LoadingPage />,
});

const Footer = dynamic(() => import("../../src/common/Footer"), { ssr: false });

export default async function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <AuthGuardWrapper>
      <Suspense fallback={<LoadingPage />}>
        <DefaultPageWrapper>
          <Nav />
          <PageView>
            <Settings>{children}</Settings>
          </PageView>
        </DefaultPageWrapper>
        <Footer />
      </Suspense>
    </AuthGuardWrapper>
  );
}
```

**Settings.tsx.**

```typescript
import { Col, Row } from 'react-flexbox-grid';
import { Typography } from '@material-tailwind/react';
import { SettingsBar } from '../common/Navbar';

function Settings({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Col xs={12} md={12} lg={12}>
        <Typography className='text-primary font-primary font-bold mb-3' variant='h2'>
          Settings
        </Typography>
      </Col>
      <Row>
        <Col xs={12} md={12} lg={4}>
          <SettingsBar />
        </Col>
        <Col xs={12} md={12} lg={8}>
          {children}
        </Col>
      </Row>
    </>
  );
}

export default Settings;
```

**LoadingPage.tsx**

```typescript
'use client';
import { Loading } from './Loading';
import { Typography } from '@material-tailwind/react';

function LoadingPage() {
  return (
    <div className='flex min-h-screen justify-center items-center flex-col'>
      <div className='pb-4'>
        <Typography variant='h3'>Just give us a second.</Typography>
      </div>
      <div className='flex justify-center items-center'>
        <Loading />
      </div>
    </div>
  );
}

export default LoadingPage;
```


So the main magic here, is the following:

```typescript
const Settings = dynamic(() => import("../../src/Pages/Settings"), {
  ssr: true,
  loading: () => <LoadingPage />, // magic here.
});
```


Lets look at how this look thus far.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfu8dxqi25v9q2n3ceti.gif)




So, as you can see, this initial loading screen is being called from these components.

```typescript
const Settings = dynamic(() => import("../../src/Pages/Settings"), {
  ssr: true,
  loading: () => <LoadingPage />, // magic here.
});

const PageView = dynamic(() => import("../../src/common/PageView"), {
  ssr: true,
  loading: () => <LoadingPage />,
});
```

Why these components ? simple, they **take the most time to load**. In Next JS you will see what components take the most time, it will be quite obvious on the UI. It will have a slight lag or flickr..

Take a look how this looks **without the loading prop declared on the ssr dynamic import component**.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tihc1vxmyj7q80ftep3m.gif)

Notice how their is a slight delay when it paints the UI ? We use the loading page to hide all of that stuff, we want to see the component after the painting process. 


_note: You could generate a [lighthouse](https://nextjs.org/learn-pages-router/seo/improve/lighthouse) report to see which components are hurting performance._


So to help mitigate UI flickering discrepancies we can pass in our loading component. That way when the component is done rendering itself, it will reveal the component without the weird flickering, or lag that occurs when using server side / client components.


So that's it for the initial loading screen. Let's circle back to the actual loading.tsx file. 

When does this get called ?

```typescript
"use client";

export default function Loading() {
  return <>Loading...</>;
}
```
Being that our structure, is structured like this

https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#layouts

The loading flow will be a bit different.  

So essentially, this specific loading.tsx file, will be shown  before the child components gets rendered


**E.g**


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3gnjqyrsysi1mlb6seha.gif)



Any other nav clicks after the first loading fallback, will not show again, because its already loaded on the network tab. 

As you can see here, 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qrgmyn5qpgadzhf97kgy.png)

loading.js does not **get called again** between nav clicks on the network tab. 


Being that loading.js is nested in our settings directory it won't reload itself in the network tab because were still within the settings folder/directory so were technically not reloading the page, we're just changing the component. 

Just navigating between these flows

/settings/account (loads its respective component)
/settings/profile  (loads its respective component)


**Update**

Earlier i mentioned you have to use suspense. It turns out you probably don't need it for loading ssr/client components. 

Although the docs mention using suspense.



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g2xho1n5dq1jams71pw8.png)

source: https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming




It works the same way without using suspense. Maybe there is other scenarios were you will need to use suspense, maybe for data fetching.


But here were just loading components, we have no data dependency for this example.

**So cleaned up code**

```typescript
"use client";

import dynamic from "next/dynamic";
import "../index.css";
import React from "react";
import AuthGuardWrapper from "../../src/common/AuthGuardWrapper";

import LoadingPage from "../../src/common/LoadingPage";

const DefaultPageWrapper = dynamic(
  () => import("../../src/common/DefaultPageWrapper"),
  { ssr: false }
);

const Settings = dynamic(() => import("../../src/Pages/Settings"), {
  ssr: true,
  // loading: () => <LoadingPage />, // i don't think we need this, PageView seems to be more expensive to load
});

const Nav = dynamic(() => import("../../src/common/Nav"), { ssr: false });

const PageView = dynamic(() => import("../../src/common/PageView"), {
  ssr: true,
  loading: () => <LoadingPage />,
});

const Footer = dynamic(() => import("../../src/common/Footer"), { ssr: false });

export default async function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <AuthGuardWrapper>
      <DefaultPageWrapper>
        <Nav />
        <PageView>
          <Settings>{children}</Settings>
        </PageView>
      </DefaultPageWrapper>
      <Footer />
    </AuthGuardWrapper>
  );
}
```



**Update Jun 2024:**

It seems that the shared gifs (that demonstrated the examples) were removed at some point. So apologies :(

At the time of writing this, i had some misconceptions on server side rendering. I realized that the above approach isn't server side friendly. 

We use 'use client' in layout, and ideally we shouldn't be using 'use client' if you're looking for server side rendering support.

You have to use 'use client' carefully so it doesn't bail out to render your pages client side. 

E.g

**BAILOUT_TO_CLIENT_SIDE_RENDERING**

_note: you will most likely see this in the markup on server tab if it bailed out_


This means you have to compartmentalize your components, such that, certain areas are for client side.


A good way to know if your next pages are rendering server side is the following:

`yarn build (next build)`
`yarn start (next start)`

Hopefully your code has compiled successfully,

Navigate to the page your making changes on, and check the network tab, and see if you see any html markup.

E.g

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vl2uxrgqrtn3vuasgwab.png)

￼
If you see html markup, then this is a good sign your page is being rendered server side, and not client side.


_**Why this is important ?**_

Well... not making use of server side rendering defeats the purpose of using next JS. Unless, you're just using it for SEO.


**Conclusion**

Hopefully this post was helpful, at first i didn't understand how to implement this, but had to dig a bit deeper.