💈

【React】手軽にページ遷移ごとのローディングバー表示を導入する

2020/11/14に公開

Zennnote などでは、ページ遷移の際にヘッダー上部にローディングバー(プログレスバー)が表示されることで、気持ちのよいUXを実現しています。

このローディングバーを自分のReactアプリに手軽に導入する方法について紹介してみようと思います。今回対象とするフレームワークは Next.js および Create React App とし、いずれも TypeScript を用いて実装していきます。

※ コードはいずれも GitHub リポジトリにアップしています (Next.js, Create React App)。

GIFだと伝わりにくいかもしれませんが、↓画像がアニメーションです。画面左から右に向けて、バーが進んでいきます。
GIF

実はローディングバー自体は、NProgress という有名なライブラリがあり、こちらを用いれば簡単にバーアニメーションを実装することができます。ですので、この NProgress を各フレームワークのどこで・どのように実装に組み込めば良いのか、という話になります。

Next.js

create-next-app で作成したプロジェクトをベースに、下記のディレクトリ・ファイル構成で考えます。

src
├── pages
│   ├── _app.tsx
│   ├── foo.tsx
│   └── index.tsx
└── styles
    └── globals.css

Next.js では、ローディングバー表示をするのに編集すべきファイルは _app.tsx のみです。
そして実装も下記だけで実現できてしまいます。

import React, { useEffect } from 'react'
import { AppProps } from 'next/app'
import nprogress from 'nprogress' // NProgressインポート
import 'nprogress/nprogress.css' // バーのデフォルトスタイルのインポート
import '../styles/globals.css'

// バーの設定
//    showSpinner: バーと一緒にローディングスピナーを表示するかどうか
//    speed: バーが右端に到達し消えるまでの時間 (msec)
//    minimum: バーの開始地点
nprogress.configure({ showSpinner: false, speed: 400, minimum: 0.25 })

const App: React.FC<AppProps> = ({ Component, pageProps }) => {
  if (process.browser) {
    // バーの表示開始
    nprogress.start()
  }

  useEffect(() => {
    // バーの表示終了
    nprogress.done()
  })

  return <Component {...pageProps} />
}

export default App

NProgress は非常に使いやすいライブラリで、バーを表示したいときに nprogress.start() を実行すると、nprogress.done() を実行するまで、ゆるやかにバーのアニメーションが進みます。
なので、コンポーネントが読み込まれ始める時に start し、コンポーネントがマウントされたら done すれば、見た目上は「ページ遷移時にローディングバーが表示される」という状態が実現できることになります。

Next.js はウェブページ上で window.document にアクセスできるようになるタイミングが if(process.browser) {} で判別できるので、

if (process.browser) {
  nprogress.start()
}

と記述するのが、バー表示に適したタイミングとなります(NProgress は document にアクセスする)。

これでバー表示の準備はできたので、あとはページ用のコンポーネントを実装していくだけです。Next.js は src/pages/ にファイルを作成すれば自動でルーティングに追加されるので、ページ遷移を試すために2ファイル作ってみましょう。

中身は何でも良いですが、ルーティングパス / に対応する index.tsx と、 /foo に対応する foo.tsx を作成し、ページ遷移できるように互いにリンクを貼ります。

// src/pages/index.tsx

import React from 'react'
import Link from 'next/link'

interface Props {}

const Home: React.FC<Props> = () => {
  return (
    <div>
      <main>
        <p>Page Home</p>
        <Link href="/foo">Go to Foo</Link>
      </main>
    </div>
  )
}

export default Home
import React from 'react'
import Link from 'next/link'

interface Props {}

const Foo: React.FC<Props> = () => {
  return (
    <div>
      <main>
        <p>Page Foo</p>
        <Link href="/">Go to Home</Link>
      </main>
    </div>
  )
}

export default Foo

以上で、ページ遷移のアニメーションが実現できます。

Create React App

Create React APP(以下、CRA)も Next.js と基本同じ考え方なのですが、

  • ルーティングを自分で定義する必要がある
  • Next.js における process.browser が使えない(存在しない)

という点が Next.js と異なり、実装もだいぶ変わってきます。

こちらも create-react-app で作成したプロジェクトをベースに、下記の構成で考えていきます。

src
├── App.css
├── App.tsx     # 編集
├── index.tsx
├── ...
├── router.tsx  # 追加
├── ...
└── pages       # 追加
    ├── foo.tsx
    └── home.tsx

今回は、まずはルーティングの定義にも必要となる各ページのコンポーネントを用意しましょう。src/pages/home.tsx および src/pages/foo.tsx をそれぞれ下記のように定義します。Next.js のときとほとんど変わりありません。

// src/pages/home.tsx

import React from 'react'
import { Link } from 'react-router-dom'

const Home: React.FC = () => {
  return (
    <div>
      <main>
        <p>Page Home</p>
        <Link to="/foo">Go to Foo</Link>
      </main>
    </div>
  )
}

export default Home
// src/pages/foo.tsx

import React from 'react'
import { Link } from 'react-router-dom'

const Foo: React.FC = () => {
  return (
    <div>
      <main>
        <p>Page Foo</p>
        <Link to="/">Go to Home</Link>
      </main>
    </div>
  )
}

export default Foo

ここで、理想的には App.tsx に NProgress の処理を書くことなのですが、残念ながら CRA ではそれを実現できません。

import React, { useState } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Home from './pages/home'
import Foo from './pages/foo'
import './App.css'

const App: React.FC = () => {

  // ここは再評価されない

  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact component={Home} />
	<Route path="/foo" exact component={Foo} />
      </Switch>
    </BrowserRouter>
  )
}

export default App

なので、代わりに全ページコンポーネントを受け取る親コンポーネントを作り、そこで NProgress の処理を行うようにします。今回は src/router.tsx というファイルを作成しています。
これにより、src/App.tsx は下記のように書き換わります。

import React from 'react'
import { BrowserRouter, Switch } from 'react-router-dom'
import Router from './router' // NProgress の処理を行うコンポーネント(実装は後述)
import Home from './pages/home'
import Foo from './pages/foo'
import './App.css'

// 全ルーティングを事前に定義しておく
interface RouterProps {
  path: string | undefined
  component: React.Component | React.FC<any>
  exact: boolean
}
const routes: RouterProps[] = [
  {
    path: '/',
    component: Home,
    exact: true,
  },
  {
    path: '/foo',
    component: Foo,
    exact: true,
  },
]

const App: React.FC = () => {
  return (
    <BrowserRouter>
      <Switch>
        // 定義しておいたルーティングを NProgress処理用コンポーネントに渡す
        {routes.map((route, index) => (
          <Router key={index} {...route} />
        ))}
      </Switch>
    </BrowserRouter>
  )
}

export default App

続いて要となる router.tsx の中身ですが、Next.js のように process.browser が CRA にはないので、これまた残念なことに Function Component は使えません。React.Component を用いることになります。実装は下記となります。

// src/router.tsx

import React from 'react'
import { Route } from 'react-router-dom'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'

nprogress.configure({ showSpinner: false, speed: 400, minimum: 0.25 })

class Router extends React.Component {
  constructor(props: any) {
    super(props)
    nprogress.start()
  }

  componentDidMount() {
    nprogress.done()
  }

  render() {
    return <Route {...this.props} />
  }
}

export default Router

constructornprogress.start() を、componentDidMountnprogress.done() を実行すれば、Next.js 同様の期待した挙動を実現できます。

実装は以上となります。今回は非同期系の処理に関しては扱っていませんが、NProgress自体はもちろん startdone の呼び出しタイミングさえ適切であれば、どのようなシーンでも用いることができるので、APIのコール・レスポンスに用いるなども可能です。適宜 hooks 化するなど考えてみると良いかもしれません。

Discussion