🤔

Link、useRouter、aタグの違いと、状態管理との共存

2023/06/30に公開

経緯

本業の方で、Next.JS 13(App RouterはまだBeta版)を用いて開発、というか既存のレガシーな作りを取っ払ってNextにreplaceしちゃおうぜ計画を行っている。
その際に技術的に気になったことをメモしていく。できれば継続的に🐟

今回はNextの機能である<Link>、Routerを用いた時のルーティングについてや、HTMLのAnchorタグルーティングとの違いや、ルーティングを行なった際、クライアントサイドでuseStateなどを用いて状態管理されているデータが更新されなかったので、その対処法。

それぞれの概要

それぞれの要素は以下のような役割を持っている。

1. Link:

Next.js が提供するコンポーネントの一つ。
これを使用すると、アプリケーション内のページ間を移動するリンクを作成できる。
Linkコンポーネントは、
クライアントサイドのナビゲーションを行い、ページ遷移は SPA(シングルページアプリケーション)のように動作する。
そのためページ全体のリロードは発生しない。

https://nextjs.org/docs/pages/api-reference/components/link

2. useRouter:

useRouter は Next.js のカスタムフックで、
現在のルートの詳細な情報(URL パラメータ、クエリパラメータなど)にアクセスしたり、プログラム的に新しいルートにナビゲートするためのメソッド(例:router.push())を提供している。
これもクライアントサイドのナビゲーションを行うため、ページ全体のリロードは発生しない。

useRouter の push メソッドを使った場合、URL は更新され、指定したページのコンポーネントが新たにレンダリングされるが、この際にサーバーへの新たなリクエストは行われず、
代わりに Next.js は(必要であれば)新たにデータをフェッチし、ページの一部を更新するらしい。

使い方の例↓

Pagination.tsx
  const router = useRouter();
  const handleNavigation = (page: number) => {
    const query = { ...router.query };
    if (page === 1) {
      delete query.page;
    } else {
      query.page = String(page);
    }

    router.push({
      pathname: router.pathname,
      query,
    });
  };

  return (
    <>
	<ul>
	  {[...Array(`ページ数合計`)].map((_, i) => (
	    <li>
	      <span onClick={() => handleNavigation(i + 1)}>{i + 1}</span>
	    </li>
	  ))}
	</ul>
    </>
  );

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

3. Anchorタグ:

標準的な HTML の <a> タグは、新しいリクエストをサーバーに送信し、新しいページを取得する。
このプロセスはサーバーサイドで行われ、ページ全体が再読み込みされる。これにより、Next.js サーバーは新しいページを完全にレンダリングすることができる。俗に言うSSR(サーバーサイドレンダリング)ってやつ。

Link、useRouterの処理の流れまとめ

認識が間違ってなければこういう流れ↓

  1. useRouterpushメソッドやLinkコンポーネントを使って新しいURLに移動することを指示。(Linkならhrefに、useRouterならpushの中にpathnameやqueryパラメータを設置して)
  2. Next.jsは、新しいURLに対応するページコンポーネントを探しにいく。
  3. もし該当のページが getServerSidePropsgetStaticProps を使用している場合、それらは新たに実行される。これらの関数はAPIを呼び出し、データを取得し、それをコンポーネントに渡す。
  4. ページコンポーネントは新たに取得したpropsを使ってレンダリングされる。

したがって、たとえpaginationなどで、同じページに遷移しても(ただし異なるパラメーターを持つ場合)、新しいURLに基づいてデータフェッチが行われ、そのデータは新しいpropsとしてコンポーネントに渡され、結果としてページは新しいデータでレンダリングされるようになっている。

Route変更の際の状態管理変数の更新

状態管理変数はそのままだと更新されない。

Routerを使って異なるパラメーターを持つ同じページに遷移した際、
useStateで状態管理している箇所だけはページが再表示されないという事象が発生した。
正確には、フェッチしたデータは取れてるけど、それが画面に再表示されることはなかった。

これが起こる原因は、クライアントサイドのナビゲーションが行われたときに、
Reactが既存のコンポーネントのインスタンスを再利用するから

コンポーネントが最初にマウントされたとき、Reactはそのコンポーネントの状態を初期化する。
しかし、そのコンポーネントが再レンダリングされるとき(例えば親から新しいpropsが渡されたときなど)、Reactは新たにコンポーネントをインスタンス化せず、既存のインスタンスとその状態を再利用する。

したがって、クライアントサイドでページを遷移させるとき、Reactは新たにページのコンポーネントをインスタンス化する代わりに既存のインスタンスを再利用し、その状態も保持する。
そして、useStateで管理されている状態も再初期化されない。
(よくよく考えたら、useRouterでのページ遷移はCSRなんだから当たり前っちゃ当たり前だった。)

解決策

新しいpropsが渡されたときにuseStateの状態を更新するためのロジックを追加する必要がある。
それを実現するには、useEffectフックを使用して行う。

コードでいうとこんな↓

  const { total } = props;
  const [totalCount, setTotalCount] = useState<number>(total);

  useEffect(() => {
    setTotalCount(total);
  }, [total]);

  return (
    <>
      <div>
        <span>{totalCount}</span>
      </div>
    </>
  );

routerを用いてクライアントサイドでナビゲーションを行い、totalの値がフェッチされ更新される(componentDidUpdate) タイミングで、setTotalCount関数でtotalCountを更新させる。

追加の余談

NextのRouterはrouter.eventsで、Router内で起こるイベントをリッスンすることができるのだが、
このイベント、Linkタグでも同じように発火するの注意だなと思った。

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

Discussion