How to setup Page Loading during page change in Next.js?

Next.js works with server side rendering to generate an optimized version of the page. While this method is great for SEO as the page comes pre-rendered, you may have noticed that the UI doesn't get updated instantly when user navigates to another page (as it takes some time for page to load).

To improve the user experience, I will show you how we can add an event listener on route change. The event listener will listen for route changes and update the state.

First, let us create a state variable that will hold the current pageLoading state

const [isPageLoading, setIsPageLoading] = useState(false);

Then, let us import the Router from next/router

import Router from 'next/router';

We can then call the event listener on the Router.

  • We attach a listener for routeChangeStart for when the change of route is initiated. In that case, we will set the pageLoading as true.

  • We attach a listener for routeChangeComplete and routeChangeError for when the change of route is completed or there is some error. In that case, we will set the pageLoading as false.

Router.events.on('routeChangeStart', () => setIsPageLoading(true));
Router.events.on('routeChangeComplete', () => setIsPageLoading(false));
Router.events.on('routeChangeError', () => setIsPageLoading(false));

These event listeners should be called in the useEffect. And once we are done with page loading, we will have to unmount the listeners. So your useEffect will look as follows:

useEffect(() => {
    const routeEventStart = () => {
        setIsPageLoading(true);
    };
    const routeEventEnd = () => {
        setIsPageLoading(false);
    };

    Router.events.on('routeChangeStart', routeEventStart);
    Router.events.on('routeChangeComplete', routeEventEnd);
    Router.events.on('routeChangeError', routeEventEnd);
    return () => {
        Router.events.off('routeChangeStart', routeEventStart);
        Router.events.off('routeChangeComplete', routeEventEnd);
        Router.events.off('routeChangeError', routeEventEnd);
    };
}, []);

Make sure to add this in your pages where you want to have a loading state. I have given a sample page below from the Houses Detail page in the dynamic meta tags article:

import React from "react";
import { useRouter } from "next/router";
import CoffeeHead from "@/components/CoffeeHead";
import Loading from "@/components/Loading";

const HouseDetail = ({ house, page_url }) => {
    const router = useRouter();
    const { id } = router.query;
    const [isPageLoading, setIsPageLoading] = useState(false);

    useEffect(() => {
        const routeEventStart = () => {
            setIsPageLoading(true);
        };
        const routeEventEnd = () => {
            setIsPageLoading(false);
        };

        Router.events.on('routeChangeStart', routeEventStart);
        Router.events.on('routeChangeComplete', routeEventEnd);
        Router.events.on('routeChangeError', routeEventEnd);
        return () => {
            Router.events.off('routeChangeStart', routeEventStart);
            Router.events.off('routeChangeComplete', routeEventEnd);
            Router.events.off('routeChangeError', routeEventEnd);
        };
    }, []);
    return (
        <>
            {pageLoading ? (
                <Loading />
            ) : (
                <CoffeeHead
                    title={house.name}
                    description={`${house.name} was founded by ${house.founder} and represented by ${house.animal}.`}
                    image={house.image}
                    page_url={page_url}
                />
                <a
                    href="#"
                    className="block max-w-md p-6 bg-white border border-gray-200 rounded-lg shadow m-auto mt-6"
                >
                    <h5 className="mb-2 text-2xl font-bold text-gray-900">
                        {house.name}
                    </h5>
                    <p className="font-normal text-gray-700">
                        Founded by {house.founder}
                    </p>
                    <p className="font-normal text-gray-700">
                        House Animal: {house.animal}
                    </p>
                </a>
            )}
        </>
    );
};

export default HouseDetail;

export const getServerSideProps = async (context) => {
    const { id } = context.query;

    const res = await fetch(
        `https://wizard-world-api.herokuapp.com/Houses/${id}`
    );
    const house = await res.json();
    house.image = `${process.env.NEXT_PUBLIC_SITE_URL}/Hogwarts-Houses.jpg`;

    const page_url = context.req.headers.host + `/houses/${id}`;

    return { props: { house, page_url } };
};

Bonus: Custom usePageLoading Hook

Adding a event listener to each and every page's useEffect is a repetitive task and is not something I prefer. So, I have create a custom hook to serve the same purpose.

import Router from 'next/router';
import { useEffect, useState } from 'react';

export const usePageLoading = () => {
    const [isPageLoading, setIsPageLoading] = useState(false);

    useEffect(() => {
        const routeEventStart = () => {
            setIsPageLoading(true);
        };
        const routeEventEnd = () => {
            setIsPageLoading(false);
        };

        Router.events.on('routeChangeStart', routeEventStart);
        Router.events.on('routeChangeComplete', routeEventEnd);
        Router.events.on('routeChangeError', routeEventEnd);
        return () => {
            Router.events.off('routeChangeStart', routeEventStart);
            Router.events.off('routeChangeComplete', routeEventEnd);
            Router.events.off('routeChangeError', routeEventEnd);
        };
    }, []);

    return { isPageLoading };
};

You can call this hook in the _app.js file in your pages folder and the loading state will be applied to all the pages.

import "@/styles/globals.css";
import { usePageLoading } from "@/hooks/use-page-loading";
import Loading from "@/components/Loading";

export default function App({ Component, pageProps }) {
    const { isPageLoading } = usePageLoading();
    return isPageLoading ? <Loading /> : <Component {...pageProps} />;
}

There you have it! We have added page loading with a custom hook to our Next.js SSR pages.

Drop a comment if you have any questions.


This page is part of the Next.js playbook, which is in the works at Coffee. Drop a mail to if you want to get on the waitlist.