DEV Community

Cover image for How to create a Preloader in Next.js
Caleb O.
Caleb O.

Posted on • Updated on

How to create a Preloader in Next.js

Before you read this article, I want you to know that it is a bit flawed, and I apologize for the mistakes I made previously, while authoring this article.

I am super thankful for the people who shared their opinions in the comment, and I learned from all of them, a great deal! You can find a new article that shows how you can add a preloader to a Next.js site, the right way, here

There's always a need to have all the content of a webpage ready before it is displayed to whoever is visiting your web app/website.

In situations where the contents of the webpage aren't ready, people would have to wait for it to be displayed, and this causes a very high decline rate of people who visit your website.

In this article, we'll have a look at how we can build a loading screen component that is displayed whenever the contents of the page is about to be mounted onto the DOM.

Before you read this article any further, you should be familiar with:

  • React, a declarative JavaScript library for building user interfaces
  • Next.js, a framework of React, used for building production-ready applications
  • Conditional rendering in React
  • Animations in CSS

Getting started

In this article, we’ll be using NextJS to set up our app, you can use create-react-app if you are not familiar with NextJS.

Let us start by installing the dependencies that we need in this project. We’d start by creating a nextjs app. The command below gets the dependencies that we need in a Nextjs app.

npx create-next-app [name-of-your-app]
Enter fullscreen mode Exit fullscreen mode

We’ll make use of the "styled-component" library to style the loading screen component. Let’s get the dependency above by typing the command below into our terminal.

npm install --save styled-components
Enter fullscreen mode Exit fullscreen mode

The components in our Nextjs app

In this section, we are going to see the different files that make up the architecture of this project, and their respective functions below.

The pages directory is where all the routing of the app takes place. This is an out-of-the-box feature of Nextjs. It saves you the stress of hard hard-coding your independent routes.

  • pages/api: the api directory enables you to have a backend for your nextjs app, inside the same codebase, instead of the common way of creating separate repositories for your REST or GraphQL APIs and deploying them on backend hosting platforms like Heroku, and so on.

  • pages/_app.js: is where all our components get attached to the DOM. If you take a look at the component structure, you’ll see that all the components are passed as pageProps to the Component props too.

function MyApp({ Component, pageProps }) {
  return (
    <React.Fragment>
      <Component {...pageProps} />
    </React.Fragment>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

It is like the index.js file in Create-React-App. The only difference here is that you are not hooking your app to the DOM node called “root”.

 React.render(document.getElementById("root"), <App />)
Enter fullscreen mode Exit fullscreen mode
  • index.js is the default route in the pages folder. When you run the command below, it starts up a development server and the contents of index.js are rendered on the web page.
npm run dev
Enter fullscreen mode Exit fullscreen mode

Building the loading screen component

The previous sections walked you through the process of installing the dependencies that are needed for building the loading screen component and the functions of each file in a typical Nextjs app.

In this section, we'll go through the step-by-step process of building the component itself.

First, we'll be taking a look at the style of the loader. We are using the styled component library for this purpose.

The Screen styled-component serves as the parent container that wraps the loading animation. It uses a fade keyframe to ensure the transition of the screen is properly utilized.

// loadingScreen.js
import styled from "styled-components";

const Screen = styled.div`
  position: relative;
  height: 100vh;
  width: 100%;
  opacity: 0;
  animation: fade 0.4s ease-in forwards;
  background: black;

  @keyframes fade {
    0% {
      opacity: 0.4;
    }
    50% {
      opacity: 0.8;
    }
    100% {
      opacity: 1;
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

The snippet below shows the Balls styled component. It serves as a container for the child elements in it. The corresponding divs in the container are the balls that we'll be animating.

You'll notice that there are unique classNames assigned to each div element in the container. This is for us to be able to set an animation-delay property on each ball so that the oscillating effect can be seen properly.

import styled from "styled-components";

const Balls = styled.div`
  display: flex;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  .ball {
    height: 20px;
    width: 20px;
    border-radius: 50%;
    background: #1b5299;
    margin: 0 6px 0 0;
    animation: oscillate 0.7s ease-in forwards infinite;
  }

  .one {
    animation-delay: 0.5s;
  }
  .two {
    animation-delay: 1s;
  }
  .three {
    animation-delay: 2s;
  }

  @keyframes oscillate {
    0% {
      transform: translateY(0);
    }
    50% {
      transform: translateY(20px);
    }
    100% {
      transform: translateY(0);
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

If you're new to animations in CSS. You can check this guide that explains the fundamentals.

Now that you have seen the styles of the components above. We'll go ahead to reference the styles in the LoadingScreeen component below.

import React from "react"
import styled from "styled-components"

const Screen = styled.div``

const Balls = styled.div``

const LoadingScreen = () => {
  return (
    <Screen>
      <Balls>
        <div className="ball one"></div>
        <div className="ball two"></div>
        <div className="ball three"></div>
      </Balls>
    </Screen>
  );
};

export default LoadingScreen;
Enter fullscreen mode Exit fullscreen mode

Implementing the preloader functionality

We've seen the function of the loading screen styles and how the animation works behind the scene.

In this section, we'll be importing the component into _app.js. Take a look at it below.

import LoadingScreen from "../src/components/LoadingScreen";

function MyApp({ Component, pageProps }) {
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    setLoading(true);
  }, []);

  return (
    <>
      {!loading ? (
        <React.Fragment>
          <Component {...pageProps} />
        </React.Fragment>
      ) : (
        <LoadingScreen />
      )}
    </>
  );
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

The snippet above shows how we've used conditional rendering to check the state of the loading component. We had to create a local state variable that holds the current state with the useState React hook.

const [loading, setLoading] = React.useState(false)
Enter fullscreen mode Exit fullscreen mode

The initial state of the loader is set to a boolean value, false.

The useEffect hook is a lifecycle method in React that is fired whenever there's a change in the DOM. It combines all the lifecycle methods that a typical class-based component will have.

  React.useEffect(() => {
    setLoading(true);
  }, []);
Enter fullscreen mode Exit fullscreen mode

By setting the initial "falsy" state of the loader to be true in the useEffect hook. We're telling the browser to display the loader.

Conclusion

Formerly, I made use of the setTimeout() function to display the loader for a significant amount of time before showing the content of the UI.

React.useEffect(() =>{
 setTimeout(() => setLoading(true), 6000);
})
Enter fullscreen mode Exit fullscreen mode

Little did I know that it was a bad practice. This practice has a lot of performance issues which in turn would result in providing a poor UX (user experience) for users visiting the web-app, as the content on the webpage could have been ready before the time that was stated in the setTimeout function.

Thankfully, Martin Vandersteen and Stephen Scaff did well by explaining to me. You can take a look at this thread to understand their ideas.

Below is a GIF that shows what we've been building all along.

preloader demo site

Although, this is an approach that helps us to have the content on a webpage/site available. If you are dealing with dynamic data, say from an API endpoint, the approach will be a little bit different.

You can decide to make use of this react-spinners package if you don't want to spend time creating a custom loading component all by yourself.

Thank you for reading this article. Kindly leave your thoughts in the comments section, and share this article with your peers, Thanks.

Top comments (21)

Collapse
 
vdsmartin profile image
Martin Vandersteen

I feel like it doesn't really work, does it ? You're only waiting 5 arbitrary seconds and since the content of the page is not in the DOM it doesn't even fetch the images etc I believe ?

The way I did it was by using a loader that goes on top of the content instead of replacing it and listening to events from imagesloaded.desandro.com/ to know when the loader could be removed

Collapse
 
seven profile image
Caleb O.

Yeah Martin...

That was kinda like the idea around it... Some DOM contents (mostly images) takes time before they're displayed on a webpage.

The idea behind the usage of the setTimeout function is to show the leader for small amount of time instead of having a blank section that holds the image or content you're expecting to see on a webpage.

I think, adding the preloading screen for a short amount of time helps to keep the users engaged on your website, before the contents of the page are fully ready.

One can also go ahead to increase or reduce the milliseconds. It all depends on your preferences, and the demography of the type of users your building for.

Collapse
 
vdsmartin profile image
Martin Vandersteen • Edited

Yes I understood that, but the way you do it won't work I think, since NextJS won't load images that are not in the DOM, so you're making your users wait for nothing I believe ? The content to load should be in the DOM but you can show a loader OVER it while you're loading.

Example :

<>
  <LoadingScreen /> // To remove/hide once it's fully loaded
  <> // Shown all the time
     <Component {...pageProps} />
  </>
</>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
seven profile image
Caleb O.

No Martin,

Nextjs already has it's own image optimization feature when they introduced the <Image /> component.

You can decide to pass properties like onBlurdataURL to display either a loader or masked image before the realy content is shown.

But the aim of this article isn't related to that. Take for example, in some countries where the access to internet service is very poor. The time that it'll take toblaod some DOM elements will be high. A preloading component comes in handy in situations like this.

Thread Thread
 
vdsmartin profile image
Martin Vandersteen

Yes, I know about the Image component but that doesn't have much to do with what I'm saying.

I don't think you understand what I'm trying to say but that's ok, my comments will be there to help people having trouble with it !

Thread Thread
 
seven profile image
Caleb O.

What will your own approach be like.

Can you share it with me? If you don't mind.

Thread Thread
 
stephenscaff profile image
Stephen Scaff

What the above commenters are saying is that your preloader isn’t actually preloading anything. It’s just waiting for a few arbitrary secs before showing the page. Assets may have loaded in less than that time, or some may have not fully loaded.

Page preloaders are generally based on a event listening to if all media has fully loaded (or completion of api request). You set an isLoading flag while your listener waits for all media to load fully, and once that’s true, isLoading can safely be set to false, so you can confidently show your page. This may take 1 sec, it may take 6 (hopefully not of course). The point is, you know for sure.

Sure 5 secs might seem like enough time for stuff to load, but you got other considerations - connection quality, device, image location/caching, etc. Conversely, your page might also be ready in 2 secs, and now your making the user wait for no reason (other than flourish).

This is more of a requirement with media and / or interaction heavy pages, where your experience requires everything is ready to ride 100%.

With Next, you’d bind that listener to useRouter’s events - routeChangeStart, routeChangeComplete.

Hope I explained that well?

Thread Thread
 
seven profile image
Caleb O.

Yes Stephen, you explained it absolutely well.

I also figured that adding the delay wasn't neccessary at all, and as you also said the page could be ready in 2 secs, and I'll be keeping the user waiting unnecessarily. Which would in turn account for a poor UX.

Thank you for your explanation. I'll make sure to add the changes in the article. 😎

Collapse
 
dunglevandesign profile image
Jun Le

i think you have misunderstood what Martin was saying. If you use conditional rendering, the page contents will be completely removed from the DOM during that 6s when loading was set to true. Which means no client side data fetching(in the page component you are hiding ofcourse), image loading etc running underneath. After 6s, loading is set to false, now the page goes to the DOM and then we will have to wait again for it to load.

Thread Thread
 
seven profile image
Caleb O.

Thank you for pointing this out too, Jun. 😎

I went through all the previous responses in this thread, and I was able to get the idea. I've made the correction in the article.

correction image

Collapse
 
desirtech profile image
Jeffrey Desir

test reply.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
seven profile image
Caleb O.

Yes, Ivan. I'm setting the loader to be visible on every page render. That's why it is inside the _app.js file.

And, yes... there's no logic that set's the loader back to false since we're making use of it in the useEffect hook, which runs only once, i.e. when the page is loaded for the first time or when it is refreshed.

Collapse
 
arshkkk profile image
arshkkk

Hardcoding the loading time isn't the best way, it's false loading
Because In nextjs production things are very fast, user will be waiting unnecessarily for each page change

Instead you can use router events in nextjs

nextjs.org/docs/api-reference/next...

Collapse
 
seven profile image
Caleb O. • Edited

Hi @arshkkk ,

I'm not making the preloading screen show on each page change. Just when the page is first mounted to the DOM.

But, I'll definitely check the link you shared here. Thank you for sharing it 😎

Collapse
 
kylessg profile image
Kyle Johnson

Here's how I did this, rather than being an arbitrary load it actually does detect images being loaded.

import Router, { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
// @ts-ignore
import AnimatedNumber from 'animated-number-react'
import NProgress from 'nprogress'
let _number = 0

const startingPoint = 20 // Loading progress on route change started
const domLoadedPoint = 35 // Loading progress when page is routed and waiting for images
const totalWait = 200 // How long to wait before going from 99% to 100%
const totalDuration = 200 // How long going from 0% - 100% takes
NProgress.configure({ easing: 'ease', speed: 200 }) // The easing of the progress bar

if (typeof document !== 'undefined') {
  NProgress.set(startingPoint / 100)
}

export default function () {
  const router = useRouter()
  const ref = useRef(router.asPath)
  const [number, setNumber] = useState<number>(35)
  const [duration, setDuration] = useState<number>(totalDuration)
  const updateNumber = (_num: number) => {
    const num = _num === 100 ? 99 : _num
    if (_num === 100) {
      setTimeout(() => {
        document
          .getElementsByTagName('html')[0]
          .classList.remove('nprogress-busy')
        setDuration(50)
        setNumber(100)
        NProgress.set(1)
        _number = 100
      }, totalWait)
    }
    setDuration(parseInt(`${((num - _number) / 100) * totalDuration}`))
    setNumber(num)
    NProgress.set(num / 100)
    _number = num
  }

  useEffect(() => {
    if (typeof document !== 'undefined') {
      const NProgress = require('nprogress')
      let timer: any

      // @ts-ignore
      // eslint-disable-next-line no-inner-declarations
      function load(route) {
        if (ref.current !== route) {
          updateNumber(startingPoint)
        }
      }

      // @ts-ignore
      // eslint-disable-next-line no-inner-declarations
      function stop(route) {
        if (!!route && route === ref.current) {
          return
        }
        if (route) {
          ref.current = route
        }
        const incompleteImages = Array.from(document.images).filter(
          (img) => !img.complete,
        )
        Promise.all(
          incompleteImages.map((img) =>
            new Promise((resolve) => {
              img.onload = img.onerror = resolve
            }).then(() => {
              updateNumber(_number + pointsPerImage)
            }),
          ),
        ).then(() => {
          updateNumber(100)
        })

        updateNumber(domLoadedPoint)

        console.log(_number)
        const pointsPerImage = (100 - domLoadedPoint) / incompleteImages.length
      }

      Router.events.on('routeChangeStart', load)
      Router.events.on('routeChangeComplete', stop)
      Router.events.on('routeChangeError', stop)
    }
    stop()
  }, [])

  return (
    <AnimatedNumber
      className='nprogress-text default'
      value={number}
      formatValue={(v: number) => `${parseInt(`${v}`)}%`}
      duration={duration}
    />
  )
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
ilirbajrami_ profile image
Ilir Bajrami • Edited

Even this new solution not working. Your useEffect function should listen to router event changes and should look like this:

 const router = useRouter();
   const [loading, setLoading] = useState(false);

 useEffect(() => {
 router.events.on("routeChangeStart", () => {
  setLoading(true);
 });

router.events.on("routeChangeComplete", () => {
  setLoading(false);
});

router.events.on("routeChangeError", () => {
  setLoading(false);
});
}, [router]);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
seven profile image
Caleb O.

Thanks Ilir! Been learning about the super power of the useRouter hook lately. I'll have this modified to reflect the change as soon as possible.

Collapse
 
manas_dev profile image
Manas Mishra

It's not actually a preloader. Preloader should have to wait till API will load, and some API's will take less time, some will take more, and using setTimeOut is actually making it static, but we need dynamic timing, which is not possible with your code.

Collapse
 
abdulrahmanelheyb profile image
Abdulrahman Elheyb

I have problem in meta tags is not rendered when page is loading. Meta Tags will be should load in the request

Collapse
 
seven profile image
Caleb O.

Hi @abdulrahmanelheyb,

Kindly take a look at this article that explains a possible fix.