Go back

Add a preloader to a Next.js site — the right way

Recently, I shared a demo of a feature that I worked on, for my website on Twitter. I know that a lot of developers like me fancy a good 'Ol loading screen in their projects. But, recently most of us have started to lose cognizance of the idea behind a loading screen.

The idea behind this particular feature in web apps is to give your users that sense of perception of what the current user interface they're requesting to see is like. It doesn't have to be all perfect, or completely like the exact reperesentaion.

Remember, the idea is to give the users perception of their actions. When we talk about actions, it could be a click event, say when a user submits a form, or when they navigate to another route in your app, and the one most folks consider as the most important — the "splash screen" that a lot of creative developers add to their landing page.

But then, this concept of loading components, is a bit beyond what we make today. A loading component should, in itself, be able to inform the user about the status of their action, it shouldn't show the text "loading", or a spinner that keeps on going till God knows when.

I mean, I know that you're trying to fetch me some resource(s), but it'll be nice if you can inform me about the action I took. Say, I clicked on the "/blog" route of your site now, I should know through the loading component that I am going to that particular route. Loading texts like "hey human, you're going to /blog", or "loading /blog", sorta gives whoever is using your app, or site a touch of empathy, and inclusiveness.

In the end, you provide a good experience for anyone that visits your site, because you have chosen to make your design accessible. This pattern isn't limited to loading screens when you're navigating from one route to another, alone. If your site is completely "content-focused" where you have tons of text, it is ideal and I'd recommend that you consider using loading skeletons.

Yes, it can be really stressful when you have to start building out the skeleton components all by yourself, like me. But there are packages that you can use to accomplish this feature. Blessing Krofegha authored a piece on how you can Implement Skeleton Screens in React. You should take a look at it.

Not every code snippet you see on Twitter is accurate.

So, what's the point of all this lore? Well, some months ago, I tried creating a loading screen on my website, and the approach I used was flawed in so many ways. From the point where it interfered with the user's choice by forcing them to wait a couple of seconds even after all the resources have been fetched by the browser, to some performance issues related to it.

I even had this notion in my mind, and I was hell-bent on sticking with that opinion, not wanting to hear from people who were telling me that it isn't the best way to solve this problem, this was because, at the time I tried implementing the feature with this approach for the first time, I was in the wilderness, YES, a literal wilderness, where there's poor internet.

That alone made my heart hardened, as though it were seared with a hot iron, because, you know, I thought I was helping people who are in the wilderness like me, by adding that "extra time" for the resources. And this was due to a snippet I found on Twitter, a while back.

The approach there was simple and straightforward, but flawed. I had to create a loading state, check if it is true or not, and if the conditions are met, return a loading component, then proceed to use the seTimeout() function to delay the render of the app component. Silly, right? Take a look at the snippet below

pages/_app.js
import React from 'react'
import LoadingScreen from '/components/LoadingScreen'
 
function MyApp({ Component, pageProps }) {
  const [loading, setLoading] = React.useState(false)
 
  React.useEffect(() => {
    setTimeout(() => setLoading(true), 6000)
  }, [])
 
  return (
    <>
      {!loading ? (
        <React.Fragment>
          <Component {...pageProps} />
        </React.Fragment>
      ) : (
        <LoadingScreen />
      )}
    </>
  )
}
 
export default MyApp

Basically, the snippet I found on Twitter can be limited to the useEffect hook in my snippet above, I modified it to suit my need. One notable error that stuck with me as I used this approach was that the social media preview of the articles on my blog was not working as it should.

I even wrote an article that warned people about using loading screens in this section. Little did I know that, because the approach in that snippet above uses conditional rendering, the page content will vanish from the DOM during that 6 seconds timeout I added with the seTimeout function, when the loading state is true.

That includes all the metadata which of course, handles the SEO-enabled preview images. The painful thing about this experience was that everything worked fine in dev mode, but not in production.

Listening to route events with Next.js UseRouter hook

The way preloading components work depends mostly on browser events. Some of these events could be related to the loading state of an image, perhaps, a request to an external API endpoint.

Six seconds, in the snippet may be a perfect time to wait for resources to load, but when we're talking about loading times, you need to consider other factors like, connection quality, the device accessing the site/app, caching and many more.

One flaw about this approach is that, your page content/resource may have been ready in three seconds, lesser than the six seconds you specified in the Timeout function, and you're in turn, making your user wait unnecessarily for another 3 seconds.

Yes, 3 seconds may appear small, but performance-wise, it isn't. You can take a look at this article that talks about Why speed matters.

With Next.js you can bind the route change events to the useRouter hook, since we're listening for route changes.

Creating a state variable that holds the loading state of the loader component, and checking to see if the route event is fired anytime the user switches to a new route, with the routeChangeStart, and routeChangeComplete method of the hook. For the sake of error handling, you can watch for when there's an error with the routeChangeError method, and render a loading error component.

pages/_app.js
import React from 'react'
import Loader from '/components/Loader'
import { useRouter } from 'next/router'
 
function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const [loading, setLoading] = React.useState(false)
 
  React.useEffect(() => {
    const handleRouteChange = (url) => {
      setLoading(true)
    }
 
    const handleRouteChangeComplete = () => {
      setLoading(false)
    }
 
    router.events.on('routeChangeStart', handleRouteChange)
    router.events.on('routeChangeComplete', handleRouteChangeComplete)
 
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
      router.events.off('routeChangeComplete', handleRouteChangeComplete)
    }
  }, [router.events])
 
  return <>{loading ? <Loader /> : <Component {...pageProps} />}</>
}
 
export default MyApp

If you ommit the handleRouteChangeComplete callback function and its usage in the router events, you'll end up having a loading component that is just there in the page, without rendering the content of the page when the resources have been completely fetched.

That's why we have the loading state set to false and passed the function as an argument to the event emitter

const handleRouteChangeComplete = () => {
  setLoading(false)
}
 
router.events.on('routeChangeComplete', handleRouteChangeComplete)

You'll also notice how the events are cleaned up in the Effect by using the off method of the event object, and lastly, the router event is passed as an argument in the dependency array of the Effect, so that it runs only when there's a route change event.

React.useEffect(() => {
  return () => {
    router.events.off('routeChangeStart', handleRouteChange)
    router.events.off('routeChangeComplete', handleRouteChangeComplete)
  }
}, [router.events])

Making the loader component dynamic

Since the idea behind a loading component isn't to render a "loading" text or a spinner, you can decide to add a bit of "dynamic-ness" to your components. An approach I took in this regard, was to let people know wherever they're navigating to, on my website.

Say, someone tries to go to the blog route, I want to show a loading text that lets the person know that they're heading to the blog route; Something like "loading /blog" should suffice. How would you accomplish this? You may ask.

The router object of Next.js has a pathname property that returns a string of the current route you're on. With that, I can use its value, and store it as a state variable in _app.js. Then, I'll make the snippet, and receive the value of the current route as a prop.

Here's what the loading component looks like.

src/components/loader/index.js
import React from 'react'
import { Item } from './style/loader.styled'
 
export const Loader = ({ loadingPathname }) => {
  return (
    <Item>
      loading
      <span className="loader-span">
        {loadingPathname === '/'
          ? '/home'
          : loadingPathname === '/blog'
          ? '/blog'
          : loadingPathname === '/articles'
          ? '/articles'
          : loadingPathname === '/projects'
          ? '/projects'
          : null}
      </span>
    </Item>
  )
}

The ternary operator in the component above, allows it to render "loading /home" when you're trying to navigate to the home page, "loading /blog" when you're going to the blog, and so on for the other routes.

So now, _app.js is modified to include the current route state. In the snippet below, you'll notice how I'm also using a package to center the loading text vertically and horizontally for consistency in the UI.

pages/_app.js
import React from 'react'
import Head from 'next/head'
import { Center } from 'centa'
import { Loader } from '@components/loader'
import { useRouter } from 'next/router'
 
function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const [route, setRoute] = React.useState(router.pathname)
  const [loading, setLoading] = React.useState(false)
 
  React.useEffect(() => {
    // ...useEffect logic
  }, [router.events])
 
  return (
    <React.Fragment>
      <Head>
        <title>Home page</title>
      </Head>
      {loading ? (
        <Center>
          <Loader loadingPathname={route} />
        </Center>
      ) : (
        <Component {...pageProps} />
      )}
    </React.Fragment>
  )
}
 
export default MyApp

Final thoughts

If the ternary operation in the loader component above seems a bit intricate, you can try thinking of it in this way; Using the if else control flow statement, as it has a higher readability for most people.

if (loadingPathname === '/') {
  setPath('/home')
} else if (loadingPathname === '/blog') {
  setPath('/blog')
} else if (loadingPathname === '/articles') {
  setPath('/articles')
} else if (loadingPathname === '/projects') {
  setPath('/projects')
}

And the texts you decide to use should not be limited to the ones mentioned in this article.

With this approach of creating a preloader, using the router events. Next.js injects <next-route-announcer> element into the dom that keeps track of how the routes are changing, and the content of the loading component goes into this element.

Take a look at what it looks like, below.

<next-route-announcer>
  <p
    aria-live="assertive"
    id="__next-route-announcer__"
    role="alert"
    style="border: 0px; 
        clip: rect(0px, 0px, 0px, 0px); 
        height: 1px; 
        margin: -1px; 
        overflow: hidden; 
        padding: 0px; 
        position: absolute; 
        width: 1px; 
        white-space: 
        nowrap; 
        overflow-wrap: normal;"
  >
    loading /blog
  </p>
</next-route-announcer>

To see this in action, you can go into the elements section in your browser’s developer tools.