DEV Community

EliHood
EliHood

Posted on • Edited on

How to handle loading screens in Next JS

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

To achieve this in Next JS, we have to make use of 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.



"use client";

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


Enter fullscreen mode Exit fullscreen mode

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

Image description

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

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

layout.tsx



"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>
  );
}


Enter fullscreen mode Exit fullscreen mode

Settings.tsx.



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;


Enter fullscreen mode Exit fullscreen mode

LoadingPage.tsx



'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;


Enter fullscreen mode Exit fullscreen mode

So the main magic here, is the following:



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


Enter fullscreen mode Exit fullscreen mode

Lets look at how this look thus far.

Image description

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



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

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


Enter fullscreen mode Exit fullscreen mode

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

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 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 ?



"use client";

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


Enter fullscreen mode Exit fullscreen mode

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

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

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

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



"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>
  );
}


Enter fullscreen mode Exit fullscreen mode

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


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.

TLDR

If you want to do loading screen on a global level, you do the following

Image description

If you want to show a loader for certain components

You do following:



import dynamic from "next/dynamic";
import LoadingPage from "../components/common/LoadingPage";

const PageView = dynamic(() => import("../components/common/PageView"), {
  ssr: true,
  loading: () => <LoadingPage />, <- add this here
});
....


Enter fullscreen mode Exit fullscreen mode

Conclusion

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

Top comments (0)