How to build a route progress bar in Next.js
Create a progress bar at the top of a Next.js application to let users know when route changes start and finish or when their requests are being processed.
Next.js makes it simple to implement static generation (SSG) and server-side rendering (SSR) in React applications. SSG pages pre-render the HTML at build time and can be served instantly from a global CDN. However, SSR pages pre-render the HTML on every request, which slows the time to the first byte (TTFB). Because this happens your users will experience SSR pages as slower in the client, although your Next.js application is processing their request on the server.
A route progress bar at the top of your Next.js application will help with the feeling of slower TTFB because it signals to your users that their route change is being processed.
Watch the lesson
Project repo
The code for this project is open-source and available on GitHub. I use Gumroad for those that want to be generous and donate, but no purchase is required. π
Next.js and Tailwind CSS starter
I chose to use Tailwind for styling my Next.js application. You can read more in the Tailwind installation guide if you're interested in how to implement Tailwind into Next.js or another React framework like Create React App or Gatsby.
I'm also using pre-built React components from Tailwind UI. They have an entire library that you can purchase, but in this lesson, I'm just using the free Tailwind UI preview components.
Zustand
In the video, I use Zustand, which is a state management library. I use Zustand because I update the progress bar state in other pages and components for downloading files, waiting on responses from the Next.js API, or any other process where I want the user to know their request is being processed. This could be achieved with React Context or another library, but I find Zustand user friendly. If you're just using the progress bar for route changes you could just use the useState React hook, which I'll cover in the _app.js
section below.
If you decide to use Zustand for your project, go ahead and install it as a dependency by running npm i zustand
in your project's terminal.
Next, create a folder called store
and create an index.js
file and useProgressStore.js
file.
The index.js
file will be used to export all the stores so it's easier to import them in other places, so all you need in the file, for now, is the following:
export * from './useProgressStore'
Inside the useProgressStore.js
file is where you'll create the store object using Zustand:
import create from 'Zustand'export const useProgressStore = create(set => ({isAnimating: false,setIsAnimating: isAnimating => set(() => ({ isAnimating })),}))
Now you have a store value named isAnimating
and a function named setIsAnimating
that will be used to update isAnimating
.
NProgress components
To help with the loading bar logic you'll be using a package called NProgress. Next.js has an official example called with-loading that uses this package, but I decided to use a React port of NProgress called react-nprogress because it makes it easier to style components with Tailwind CSS class names.
Next, install the React port of NProgress by running npm i react-nprogress
in your project's terminal. Now create a folder called components
and another folder called progress
. Create an index.js
file and export everything from the progress
folder by writing the following code:
export * from './progress'
The progress
folder will have four more files: index.js
, Bar.js
, Container.js
, and Progress.js
. The index.js
file is going to export everything from the Progress component, so all you need to write inside is the following code:
export * from './Progress'
Bar component
In your Bar.js
file write the following code:
export const Bar = ({ animationDuration, progress }) => (<divclassName="bg-indigo-600 fixed left-0 top-0 z-50 h-1 w-full"style={{marginLeft: `${(-1 + progress) * 100}%`,transition: `margin-left ${animationDuration}ms linear`,}}></div>)
The Bar component will take two props, which it will get from the useNProgress
hook (covered later). The styling is taken care of by Tailwind CSS class names.
Container component
In your Container.js
file write the following code:
export const Container = ({ animationDuration, children, isFinished }) => (<divclassName="pointer-events-none"style={{opacity: isFinished ? 0 : 1,transition: `opacity ${animationDuration}ms linear`,}}>{children}</div>)
The Container component will be used to wrap the Bar component. It will use the isFinished
prop from the useNProgress
hook to control opacity and the animationDuration
prop to control the opacity animation.
Progress component
With the Bar and Container component complete you can write the following in your Progress.js
file:
import { useNProgress } from '@tanem/react-nprogress'import { Bar } from './Bar'import { Container } from './Container'export const Progress = ({ isAnimating }) => {const { animationDuration, isFinished, progress } = useNProgress({isAnimating,})return (<Container animationDuration={animationDuration} isFinished={isFinished}><Bar animationDuration={animationDuration} progress={progress} /></Container>)}
The Progress component calls the useNProgress
hook from react-nprogress
. This hook gives you access to values from NProgress, which will be needed to pass into the Container and Bar components as props.
The only prop that's passed into the Progress component is isAnimating
, which will come from useProgressStore
or useState
.
Add the Progress component
With the Progress component complete you can now import it and use it in your _app.js
file. By using it in the _app.js
file the Progress component will be rendered on every page.
As mentioned in the previous section, the Progress component only takes one prop: isAnimating
. Below are examples of how to pass that prop into the Progress component depending on if you used Zustand or useState
.
Zustand
Write the following code in your _app.js
file if you used Zustand to manage isAnimating
:
import 'tailwindcss/tailwind.css'import { Progress } from '/components'import { useProgressStore } from '/store'function MyApp({ Component, pageProps }) {const setIsAnimating = useProgressStore(state => state.setIsAnimating)const isAnimating = useProgressStore(state => state.isAnimating)return (<><Progress isAnimating={isAnimating} /><Component {...pageProps} /></>)}export default MyApp
useState
Write the following code in your _app.js
file if you used useState
to manage isAnimating
:
import { useState } from 'react'import 'tailwindcss/tailwind.css'import { Progress } from '/components'function MyApp({ Component, pageProps }) {const [isAnimating, setIsAnimating] = useState(false)return (<><Progress isAnimating={isAnimating} /><Component {...pageProps} /></>)}export default MyApp
Router events
This section is focused on setting the isAnimating
state based on router events, which you can get access to from a Next.js hook called useRouter
. You'll also pair that with the useEffect
React hook, which will help make DOM updates.
The code for implementing this will also be in your _app.js
file, but will also depend on if you used Zustand or useState
. Below are examples for each.
Zustand
The code in your _app.js
file should be the following if you used Zustand:
import { useEffect } from 'react'import 'tailwindcss/tailwind.css'import { useRouter } from 'next/router'import { Progress } from '/components'import { useProgressStore } from '/store'function MyApp({ Component, pageProps }) {const setIsAnimating = useProgressStore(state => state.setIsAnimating)const isAnimating = useProgressStore(state => state.isAnimating)const router = useRouter()useEffect(() => {const handleStart = () => {setIsAnimating(true)}const handleStop = () => {setIsAnimating(false)}router.events.on('routeChangeStart', handleStart)router.events.on('routeChangeComplete', handleStop)router.events.on('routeChangeError', handleStop)return () => {router.events.off('routeChangeStart', handleStart)router.events.off('routeChangeComplete', handleStop)router.events.off('routeChangeError', handleStop)}}, [router])return (<><Progress isAnimating={isAnimating} /><Component {...pageProps} /></>)}export default MyApp
useState
The code in your _app.js
file should be the following if you used useState
:
import { useState, useEffect } from 'react'import 'tailwindcss/tailwind.css'import { useRouter } from 'next/router'import { Progress } from '/components'function MyApp({ Component, pageProps }) {const [isAnimating, setIsAnimating] = useState(false)const router = useRouter()useEffect(() => {const handleStart = () => {setIsAnimating(true)}const handleStop = () => {setIsAnimating(false)}router.events.on('routeChangeStart', handleStart)router.events.on('routeChangeComplete', handleStop)router.events.on('routeChangeError', handleStop)return () => {router.events.off('routeChangeStart', handleStart)router.events.off('routeChangeComplete', handleStop)router.events.off('routeChangeError', handleStop)}}, [router])return (<><Progress isAnimating={isAnimating} /><Component {...pageProps} /></>)}export default MyApp
Both examples get access to the router object by calling the useRouter
hook from next/router
. The router events routeChangeStart
, routeChangeComplete
, and routeChangeError
are listened to in the useEffect
hook and will either call handleStart
or handleStop
if triggered. Both of these functions will change the isAnimating
state value to false
or true
.
You will need to stop listening for the events in the useEffect
cleanup to prevent unnecessary behavior or memory leaking issues. The Next.js router object makes this simple to do by calling router.events.off()
, as opposed to router.events.on()
. For clarity, your cleanup function is the following part in the useEffect
hook:
return () => {router.events.off('routeChangeStart', handleStart)router.events.off('routeChangeComplete', handleStop)router.events.off('routeChangeError', handleStop)}
The last step to note is the router
argument in the useEffect
dependency array. By including router
as an argument you're telling useEffect
to watch for any changes to the router object. If a change occurs then the useEffect
hook will recall the effect functions.
Conclusion
In this lesson, you learned how to create a simple progress bar with a React port of NProgress and how to listen to router events using the useRouter
and useEffect
hooks. Now when your users request a server-side rendered page with Next.js you can rest assured they're not wondering if their page request is being processed.