Page Loading Progress with Next.js and Chakra UI
Tags: nextjs, chakra, progress
Published: 2021-05-30 18:13:43

# Motivation

We all love [NProgress](https://ricostacruz.com/nprogress/) library, but since we are already using Chakra UI which has build-in `Progress` and `CircularProgress` components let's try to build a page loading progress bar our self.

![NProgress Library](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kaslh2hmapx1szfz5xhs.gif)

We will build the progress bar which shows the page loading progress when we move from one page to another. It will look like this.

![Loading progress result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9i287bgpvwg04ivksuff.gif)

# Set up

Create a new Next.js project. You could use [Start New Next.js Project Notes](https://dev.to/vladimirvovk/start-new-next-js-project-notes-13m3) or clone [this repo's](https://github.com/vladimir-vovk/nextjs-starter) `empty-project` branch.

# Chakra UI

Now we are ready to start. Let's [add Chakra UI](https://chakra-ui.com/docs/getting-started) to our project first.

Install Chakra UI by running this command:
```
yarn add @chakra-ui/react '@emotion/react@^11' '@emotion/styled@^11' 'framer-motion@^4'
```

For Chakra UI to work correctly, we need to set up the `ChakraProvider` at the root of our application `src/pages/_app.tsx`.

```typescript
import { AppProps } from 'next/app'
import { ReactElement } from 'react'

import { ChakraProvider } from '@chakra-ui/react'

function MyApp({ Component, pageProps }: AppProps): ReactElement {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  )
}

export default MyApp

```

Save changes and reload the page. If you see that styles have changed then it's a good sign.

# Layout

Usually, we have some common structure for all our pages. So let's add a `Layout` component for that. Create a new `src/ui/Layout.tsx` file.

```typescript
import { ReactElement } from 'react'
import { Flex } from '@chakra-ui/react'

type Props = {
  children: ReactElement | ReactElement[]
}

const Layout = ({ children, ...props }: Props) => {
  return (
    <Flex direction="column" maxW={{ xl: '1200px' }} m="0 auto" p={6} {...props}>
      {children}
    </Flex>
  )
}

export default Layout
```

Nothing fancy here. Just a `div` ([Flex](https://chakra-ui.com/docs/layout/flex) Chakra component) with `display: flex`, `max-width: 1200px` for wide screens, `margin: 0 auto`, and `padding: 24px` (6 * 4px).

Also it's convenient to import our UI components from the `src/ui`. Let's add `src/ui/index.ts` export file for that.

```typescript
export { default as Layout } from './Layout'
```

Now we are ready to add our `Layout` to `src/pages/_app.tsx`.

```typescript
// ... same imports as before, just add import of Layout
import { Layout } from 'src/ui'

function MyApp({ Component, pageProps }: AppProps): ReactElement {
  return (
    <ChakraProvider>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </ChakraProvider>
  )
}

export default MyApp
```

Reload the page. You should see margins now. Cool! Let's move to the progress bar. 🎊

# Progress bar

Ok, let's think for a moment. We need to be able to control our progress bar from any part of our application, right? Imaging pressing a button inside any page to show progress. React has a build-in abstraction for that called [Context](https://reactjs.org/docs/context.html).

> Context provides a way to pass data through the component tree without having to pass props down manually at every level.

If it tells nothing for you don't panic. Bare with me it will make sense soon. 🤓

First, we should create a "context" and "context provider". Wrap our application's component tree with " context provider" which will allow us to use data and methods that shared by our "context". Create a new file `src/services/loading-progress.tsx` which will contain all the code that we need.

```typescript
import { Progress, VStack, CircularProgress } from '@chakra-ui/react'
import { createContext, ReactElement, useContext, useState, useEffect, useRef } from 'react'

type Props = {
  children: ReactElement | ReactElement[]
}

type Progress = {
  value: number
  start: () => void
  done: () => void
}

// 1. Creating a context
const LoadingProgressContext = createContext<Progress>({
  value: 0,
  start: () => {},
  done: () => {}
})

// 2. useLoadingProgress hook
export const useLoadingProgress = (): Progress => {
  return useContext<Progress>(LoadingProgressContext)
}

// 3. LoadingProgress component
const LoadingProgress = () => {
  const { value } = useLoadingProgress()

  return (
    <VStack align="flex-end" position="absolute" top={0} left={0} right={0}>
      <Progress value={value} size="xs" width="100%" />
      <CircularProgress size="24px" isIndeterminate pr={2} />
    </VStack>
  )
}

// 4. LoadingProgressProvider
export const LoadingProgressProvider = ({ children }: Props): ReactElement => {
  // 5. Variables
  const step = useRef(5)
  const [value, setValue] = useState(0)
  const [isOn, setOn] = useState(false)

  // 6. useEffect
  useEffect(() => {
    if (isOn) {
      let timeout: number = 0

      if (value < 20) {
        step.current = 5
      } else if (value < 40) {
        step.current = 4
      } else if (value < 60) {
        step.current = 3
      } else if (value < 80) {
        step.current = 2
      } else {
        step.current = 1
      }

      if (value <= 98) {
        timeout = setTimeout(() => {
          setValue(value + step.current)
        }, 500)
      }

      return () => {
        if (timeout) {
          clearTimeout(timeout)
        }
      }
    }
  }, [value, isOn])

  // 7. start
  const start = () => {
    setValue(0)
    setOn(true)
  }

  // 8. done
  const done = () => {
    setValue(100)
    setTimeout(() => {
      setOn(false)
    }, 200)
  }

  return (
    <LoadingProgressContext.Provider
      value={{
        value,
        start,
        done
      }}
    >
      {isOn ? <LoadingProgress /> : <></>}
      {children}
    </LoadingProgressContext.Provider>
  )
}
```

Wow! Let's break it down.

## 1. Creating a context

First we need to create a context with some default variables. We will change this values later.

```typescript
// 1. Creating a context
const LoadingProgressContext = createContext<Progress>({
  value: 0,
  start: () => {},
  done: () => {}
})
```

We will use `value` as a progress percentage. It should be a number from 0 to 100. `start` function will show the progress bar and set timeout to increment `value`. `done` function will set the `value` to 100 and hide the progress bar.

## 2. useLoadingProgress hook

```typescript
// 2. useLoadingProgress hook
export const useLoadingProgress = (): Progress => {
  return useContext<Progress>(LoadingProgressContext)
}
```

This function will return context values and methods, so we could use them anywhere in our app. We will learn how to use it later.

## 3. LoadingProgress component

```typescript
// 3. LoadingProgress component
const LoadingProgress = () => {
  const { value } = useLoadingProgress()

  return (
    <VStack align="flex-end" position="absolute" top={0} left={0} right={0}>
      <Progress value={value} size="xs" width="100%" />
      <CircularProgress size="24px" isIndeterminate pr={2} />
    </VStack>
  )
}
```

This is our progress bar component. It consists of two Chakra UI components [`Progress`](https://chakra-ui.com/docs/feedback/progress) and [`CircularProgress`](https://chakra-ui.com/docs/feedback/circular-progress) combined inside vertical stack [`VStack`](https://chakra-ui.com/docs/layout/stack). Vertical stack has an absolute position with `top: 0; left: 0; right: 0` which means that it will be on the top of our page.

## 4. LoadingProgressProvider

The main purpose of this function is to wrap our application's components tree and share values and methods with them.

```typescript
// ... the most important part
return (
    <LoadingProgressContext.Provider
      value={{
        value,
        start,
        done
      }}
    >
      {isOn ? <LoadingProgress /> : <></>}
      {children}
    </LoadingProgressContext.Provider>
  )
```

Here we see that "provider" will return the component which wraps all children components and renders them. Share `value` value, `start` and `done` methods. Renders `LoadingProgress` component depends on if it's on or off now.

### 5. Variables

```typescript
  // 5. Variables
  const step = useRef(5)
  const [value, setValue] = useState(0)
  const [isOn, setOn] = useState(false)
```

We will use `step` to change the speed of the progress bar growth. It will grow faster in the beginning and then slow down a little bit.

`value` will contain the progress bar value. Which is from 0 to 100.

`isOn` variable will indicate if the progress bar is visible now.

### 6. useEffect

```typescript
  // 6. useEffect
  useEffect(() => {
    if (isOn) {
      let timeout: number = 0

      if (value < 20) {
        step.current = 5
      } else if (value < 40) {
        step.current = 4
      } else if (value < 60) {
        step.current = 3
      } else if (value < 80) {
        step.current = 2
      } else {
        step.current = 1
      }

      if (value <= 98) {
        timeout = setTimeout(() => {
          setValue(value + step.current)
        }, 500)
      }

      return () => {
        if (timeout) {
          clearTimeout(timeout)
        }
      }
    }
  }, [value, isOn])

```

This function will run when the app starts and when the `value` or `isOn` variable will change. It will set the `step` variable depends on the current `value` value. Remember we want our progress bar to slow down at the end. Then if the `value` is less than 98 we will set a timeout for 500 milliseconds and increase the `value` by `step`. Which will trigger the `useEffect` function again, because `value` was changed.

### 7. start

This function will reset the progress bar to 0 and make it visible.

### 8. done

This function will set the progress bar value to 100 and hides it after 200 milliseconds.


Huh! This module was tough. But we are almost ready to use our progress bar!

## Export 

One more thing left here is to export our new code. Create a file `src/services/index.ts`.

```typescript
export { LoadingProgressProvider, useLoadingProgress } from './loading-progress'
```

And add `LoadingProgressProvider` to our application components tree. To do that, open the `src/pages/_app.tsx` file and add `LoadingProgressProvider` there.

```typescript
function MyApp({ Component, pageProps }: AppProps): ReactElement {
  return (
    <ChakraProvider>
      <LoadingProgressProvider>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </LoadingProgressProvider>
    </ChakraProvider>
  )
}

```

# Add router change events to Layout

Now we need to listen to `Router` events and when the page changes show the progress bar we just created. To do that open `src/ui/Layout.tsx` and add these imports.

```typescript
import { useEffect } from 'react'
import Router from 'next/router'
import { useLoadingProgress } from 'src/services'
```

Then we need to add this code at the top of the `Layout` function.

```typescript
// ...
const Layout = ({ children, ...props }: Props) => {
  // 1. useLoadingProgress hook
  const { start, done } = useLoadingProgress()

  // 2. onRouterChangeStart
  const onRouteChangeStart = () => {
    start()
  }

  // 3. onRouterChangeComplete
  const onRouteChangeComplete = () => {
    setTimeout(() => {
      done()
    }, 1)
  }

  // 4. Subscribe to router events
  useEffect(() => {
    Router.events.on('routeChangeStart', onRouteChangeStart)
    Router.events.on('routeChangeComplete', onRouteChangeComplete)
    Router.events.on('routeChangeError', onRouteChangeComplete)

    return () => {
      Router.events.off('routeChangeStart', onRouteChangeStart)
      Router.events.off('routeChangeComplete', onRouteChangeComplete)
      Router.events.off('routeChangeError', onRouteChangeComplete)
    }
  }, [])

  return (
    <Flex direction="column" maxW={{ xl: '1200px' }} m="0 auto" p={6} {...props}>
      {children}
    </Flex>
  )
}
```

## 1. useLoadingProgress hook

`useLoadingProgress` hook will return us the `start` and `done` methods from our `LoadingProgessProvider` so that we could start and stop our progress bar.

## 2. onRouterChangeStart

This function will show and start the progress bar.

## 3. onRouterChangeComplete

This function will set the progress bar value to 100 and hide it after 200 milliseconds. Noticed that it wrapped with `setTimeout`. This needed to delay the `done` function a little bit. Otherwise, we can't see anything if a page will change quickly. Which is the case with Next.js. 😆💪🏻

## 4. Subscribe to router events

Here we will use `useEffect` function to subscribe and unsubscribe to `routeChangeStart`, `routeChangeComplete` and `routeChangeError` Next.js `Router` events.

# Test time!

Let's add a second page just for the test case. Create a new  `src/pages/second-page.tsx` file for that.

```typescript
const SecondPage = () => <h1>Hey! I'm a second page!</h1>

export default SecondPage
```

Then let's add a link to the second page from our index page. Open `src/pages/index.tsx` and add a `Link` import on top.

```typescript
import Link from 'next/link'
```

Then add `Link` to the index page body.

```typescript
export default function Home() {
  return (
    <div>
      // ...

      <main>
        // ...

        <Link href="/second-page">Second page</Link>
      </main>
    </div>
  )
}
```

Now reload the index page and try to press on the `Second page` link.

![Loading progress result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9i287bgpvwg04ivksuff.gif)

Cool right! 🥳

Want to see how it will look like for a slow Internet connection? Good! Just add these imports to `src/pages/index.tsx` file.

```typescript
import { Button, Box } from '@chakra-ui/react'
import { useLoadingProgress } from 'src/services'
```

Add this code on the top of the `Home` function before `return`.

```typescript
const { start } = useLoadingProgress()
```

And add this code below our link to the second page.

```typescript
<Box>
  <Button mr={4} onClick={() => start()}>
    Start
  </Button>
</Box>
```

Reload the index page, press the `Start` button and enjoy! 🌈

![Loading progress on slow Internet connection](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/era9qjlquz2n230g9dvg.gif)

That's all. 😊
Check out the [repo](https://github.com/vladimir-vovk/nextjs-starter/tree/loading-progress), subscribe and drop your comments below! ✌🏻


# Credits

Photo by <a href="https://unsplash.com/@emeritte82?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ermelinda Martín</a> on <a href="https://unsplash.com/s/photos/chakra?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>