【React】手軽にページ遷移ごとのローディングバー表示を導入する
Zenn や note などでは、ページ遷移の際にヘッダー上部にローディングバー(プログレスバー)が表示されることで、気持ちのよいUXを実現しています。
このローディングバーを自分のReactアプリに手軽に導入する方法について紹介してみようと思います。今回対象とするフレームワークは Next.js および Create React App とし、いずれも TypeScript を用いて実装していきます。
※ コードはいずれも GitHub リポジトリにアップしています (Next.js, Create React App)。
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
constructor
で nprogress.start()
を、componentDidMount
で nprogress.done()
を実行すれば、Next.js 同様の期待した挙動を実現できます。
実装は以上となります。今回は非同期系の処理に関しては扱っていませんが、NProgress自体はもちろん start
と done
の呼び出しタイミングさえ適切であれば、どのようなシーンでも用いることができるので、APIのコール・レスポンスに用いるなども可能です。適宜 hooks 化するなど考えてみると良いかもしれません。
Discussion