Sometimes when switching between pages, Next.js needs to download pages from the server before rendering the page. This will happen on SSR projects especially, And it may also need to wait for the data. So while doing these tasks, the browser might be non-responsive.
We can simply fix this issue by showing a loading indicator.
Let's do it without wasting time:
I have created a simple Next.js project with typescript and my _app.tsx
file is looks like this:
// src/_app.tsx
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
I extremely love MUI components, so have tried to create a layout that contains a header and navigation menu:
// src/components/Header.tsx
import {useRouter} from "next/router";
import {AppBar, Box, Button, Typography} from '@mui/material';
const pages = [
{label: 'Home', path: "/"},
{label: 'Users', path: "/users"},
{label: 'Posts', path: '/posts'},
];
const AppHeader = () => {
const router = useRouter();
return (
<AppBar position="static" sx={{px:1}}>
<Box sx={{display: 'flex',alignItems:'center'}}>
<Typography
variant="h6"
component="a"
sx={{
mr: 2,
display: {xs: 'none', md: 'flex'},
color: 'inherit',
}}
>
Next.js example app with route loading
</Typography>
<Box sx={{ display:'flex'}}>
{pages.map((page) => (
<Button
key={page.label}
color={'inherit'}
sx={{my: 2,display: block',textTransform:'none'}}
onClick={() => router.push(page.path)}
>
{page.label}
</Button>
))}
</Box>
</Box>
</AppBar>
);
}
export default AppHeader;
// src/components/Layout.tsx
import {ReactNode} from "react";
import Head from 'next/head'
import dynamic from 'next/dynamic'
const Header = dynamic(() => import('./Header'))
export default function Layout({children}: { children: ReactNode }) {
return (
<>
<Head>
<title>Magnificent</title>
</Head>
<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
padding: 1rem 10rem 10rem 1rem;
}
`}</style>
<Header/>
<div className={'container'}>{children}</div>
</>
)
}
of course, the _app.tsx should be like:
// src/_app.tsx
import dynamic from "next/dynamic";
import type {AppProps} from 'next/app'
const Layout = dynamic(() => import('@/components/Layout'))
export default function App({Component, pageProps}: AppProps) {
return <Layout><Component {...pageProps} /></Layout>
}
Cool, now let's write "Users" and "Posts" components that using getServerSideProps
. Next.js will pre-render this page on each request using the data returned by getServerSideProps
.
Each one should fetch some data from a mock server that we use jsonplaceholder here. The components are:
// src/pages/users.tsx
import axios from "axios";
import {Typography} from "@mui/material";
import {IUsers} from "@/utils/users-type";
export async function getServerSideProps() {
const res = await axios('https://jsonplaceholder.typicode.com/users')
const users = await res.data
return {
props: {
users
}
}
}
type UsersProps = {
users: IUsers[]
}
const Users = ({users}: UsersProps) => {
return (
<>
{
users?.map((user: IUsers) => <Typography variant= {'subtitle1'}>{user.name}</Typography>)
}
</>
)
}
export default Users;
// src/pages/posts
import axios from "axios";
import {Typography} from "@mui/material";
import {IPosts} from "@/utils/posts-type";
export async function getServerSideProps() {
const res = await axios('https://jsonplaceholder.typicode.com/posts')
const posts = await res.data
return {
props: {
posts
}
}
}
type PostsProps = {
posts: IPosts[]
}
const Posts = ({posts}: PostsProps) => {
return (
<>
{
posts?.map((post: IPosts) => <Typography variant={'subtitle1'}>{post.title}</Typography>)
}
</>
)
}
export default Posts;
What we've done for now is, retrieved users and show the name of users, and the same way for posts and show the title of posts. It's time to put a loader that will be shown when changing the page, let's install nprogress:npm i nprogress
or yarn add nprogress
and for those who are using typescript, also need to install types:npm i @types/nprogress
or yarn add @types/nprogress
The best place that we could implement this, is _app.tsx
file, we could listen to different events happening inside the Next.js Router:
// src/_app.tsx
import {useEffect} from "react";
import dynamic from "next/dynamic";
import type {AppProps} from 'next/app'
import {useRouter} from "next/router";
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
NProgress.configure({showSpinner: false});
const Layout = dynamic(() => import('@/components/Layout'))
export default function App({Component, pageProps}: AppProps) {
const router = useRouter()
useEffect(() => {
const handleStart = (url: string) => {
console.log(`Loading: ${url}`)
NProgress.start()
}
const handleStop = () => {
NProgress.done()
}
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 <Layout><Component {...pageProps} /></Layout>
}
import nprogress package and css file
set the progress functionality with Next.js Router events in a react useEffect hook with router dependency. Now once the page starts to change, progress bar will be shown on top and will be disappear when the page change completed.
and finally, we could also write some styles for the progress bar, I did it into
Layout.tsx
file:
// src/components/Layout.tsx
import {ReactNode} from "react";
// Next.js
import Head from 'next/head'
import dynamic from 'next/dynamic'
// Project imports
const Header = dynamic(() => import('./Header'))
//======================|| Main Layout ||===========================
export default function Layout({children}: { children: ReactNode }) {
return (
<>
<Head>
<title>Magnificent</title>
</Head>
<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
padding: 1rem 10rem 10rem 1rem;
}
#nprogress .bar {
background: white !important;
height: 4px !important;
}
#nprogress .peg {
box-shadow: 0 0 10px rgba(243, 242, 240, 0.49), 0 0 5px #d2d2d1;
}
`}</style>
<Header/>
<div className={'container'}>{children}</div>
</>
)
}
And done. Thanks.
Source code: https://github.com/hamitmohamadi/next-app-with-route-loading