🔜

【Next.js / App Router】Linkコンポーネントによるページ遷移前にプログレスバーを表示させたい!

2024/03/17に公開

こんにちは!@Ryo54388667です!☺️

普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

今回はLinkコンポーネントによるページ遷移前にプログレスバーを表示する方法について紹介したいと思います!

📌 課題

まず、遷移前のプログレスバーがどのようなものかというと、次の動画のものです。
GitHubのGUIにも用いられていて、タブを切り替える際、画面の左上から右上にかけて青色のバーが横断する形で延伸します。

このUIは処理が進行中であることをユーザーに伝える役割を担っています。遷移先のページが初期表示に時間がかかる場合に頻繁に利用されます。

こちらの実装方法ですが、App Router以前は、_app.tsxにNext.jsの組み込みライブラリーのnext/routerのrouter.eventsを利用して実装することが多かったです。Web上の記事でもそのような実装例が多いですね。

import { useEffect } from 'react'
import { useRouter } from 'next/router'
 
export default function MyApp({ Component, pageProps }) {
  const router = useRouter()
 
  useEffect(() => {
    const handleRouteChangeError = (err, url) => {
      if (err.cancelled) {
        console.log(`Route to ${url} was cancelled!`)
      }
    }
 
    router.events.on('routeChangeError', handleRouteChangeError)
 
    // If the component is unmounted, unsubscribe
    // from the event with the `off` method:
    return () => {
      router.events.off('routeChangeError', handleRouteChangeError)
    }
  }, [router])
 
  return <Component {...pageProps} />
}

https://nextjs.org/docs/pages/api-reference/functions/use-router#routerevents

しかし、App Router 標準のnext/navigation のrouter機能には以前のrouter.eventsに相当するようなものがありません😇詳しくは下記のDiscussionを見てください!
https://github.com/vercel/next.js/discussions/41934

Pages RouterからApp Routerへ大きくインターフェースが変更されたので仕方のないことではありますが、後方互換性がないのは少々ツラいですね。。

📌 実装について

結論

今回、プログレスバー自体の実装についてはスコープ外なのでライブラリーを利用させていただきます。NProgressというライブラリーを利用します。

Package

name version
Next 14.0.0
React 18.2.0
nprogress 0.2.0

まず結論ですが、

/components/navigation-events.tsx

'use client'
import { useEffect } from 'react'
import NProgress from "nprogress";
import 'nprogress/nprogress.css'

type PushStateInput = [data: any, unused: string, url?: string | URL | null | undefined];

export function NavigationEvents() {

  const handleAnchorClick = () => {
    NProgress.start()
  };

  const handleMutation: MutationCallback = () => {
    const anchorElements = document.querySelectorAll('a');
    anchorElements.forEach((anchor) => anchor.addEventListener('click', handleAnchorClick));
  };

  useEffect(() => {
    const mutationObserver = new MutationObserver(handleMutation);
    mutationObserver.observe(document, { childList: true, subtree: true });

    window.history.pushState = new Proxy(window.history.pushState, {
      apply: (target, thisArg, argArray: PushStateInput) => {
        NProgress.done();
        return target.apply(thisArg, argArray);
      },
    });
  }, [])

  return null
}

/app/layout.tsx

import { Suspense } from 'react'
import { NavigationEvents } from './components/navigation-events'

 export default function Layout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Suspense fallback={null}>
          <NavigationEvents />
        </Suspense>
      </body>
    </html>
  )
}

詳細

なかばパワープレイで恐縮ですが、全体の流れは次のような感じです。

  1. DOMの監視を行い、変更を検知する
  2. ページ内のaダグを取得し、clickイベントを追加する
  3. historyAPIの挙動の中にプログレスバーの処理を追加する

今回、最も単純な形で実装をしました。

実際のところ、まだ課題はたくさんあります!
たとえば、aタグの中にはtel:のように電話番号を呼び出すものもあるので、それを考慮する必要があります。さらに、target属性が_blankの場合はプログレスバーは不要です。

今回はdocument全体を監視の対象としているのですが、意図しない副作用があるかもしれないので特定のDOMに絞ることも必要かと思います。

今回、手弁当で実装してみましたが、実務で利用するなら考慮事項も多いので素直にライブラリーを利用するのをおすすめします!ここにきて、ちゃぶ台返しもいいところですが。。笑

僕が調べた中ではこちらのライブラリーが良さそうです✨インターフェースもシンプルですし。
https://github.com/TheSGJ/nextjs-toploader

参考

📌 まとめ

  1. aタグにクリックイベントを追加し、プログレスバーのスタート処理を追加する。
  2. historyAPIにプラグレスバーのエンド処理を追加する。

より良い方法があれば教えてください〜

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667/status/1733434994016862256

Discussion