React RouterでReact SPAのページ遷移時のスクロール位置を制御する

はじめに

この記事はフラー株式会社 Advent Calendar 202220日目の記事です。 qiita.com

19日目は @canacelさんで「canacel.net を開設しました」でした。

ReactでSPAを構築する際には、React Routerを使用します。 React RouterはURLに合わせてコンポーネントの出し分けを可能にするライブラリです。

import { createBrowserRouter} from "react-router-dom";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "next", element: <Next /> },
]);

上記のコードでは、ルートURL("/")ではHomeコンポーネントが、/nextではNextコンポーネントレンダリングされます。 それぞれのコンポーネントが各ページの内容を描画するようにすれば、ページ遷移が実現できます。 今回はReact Routerを用いたページ遷移におけるスクロール位置の制御について検証します。

検証環境

React: v.18.2.0

React Router: v.6.5.0

デフォルトでは、前画面のスクロール位置が継承される

まずは以下のコードでの動作を確認します。

import { css } from "@emotion/css";
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, Link, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "next", element: <Next /> },
]);

function Home() {
  return (
    <div
      className={css`
        height: 200vh; // スクロールが発生するようにウィンドウ縦幅の2倍の高さに設定
        width: 100vw;
        padding-bottom: 200px;
        background: linear-gradient(
          blue,
          pink
        ); // スクロールがわかりやすいように背景をグラデーションにする
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        overflow: auto;
      `}
    >
      <h1>Home</h1>
      <Link to={"/next"}>Click to Next page.</Link>
    </div>
  );
}

function Next() {
  return (
    <div
      className={css`
        height: 200vh;
        width: 100vw;
        padding-bottom: 200px;
        background: linear-gradient(blue, green);
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        overflow: auto;
      `}
    >
      <h1>Next</h1>
      <Link to={"/"}>Click to Home page.</Link>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

実装内容を簡潔に図示すると下記のようになります。 縦長のページの下部に見出しと別ページへのリンクがあり、二つのページを行き来するアプリケーションです。 React Routerでのスクロールに関する制御は、何も行っていません。 以下のような挙動になります。 HomeからNextへ移る際、いきなりページ下部に配置された見出しやHomeへのリンクが見えているので、前ページのスクロール位置が継承されていることがわかります。 スクロール自体はHTMLのbody要素が制御していて、React Routerはbody直下のdivに含まれる要素の出し分けをしているからだと考えられます。

ScrollRestorationコンポーネントでスクロール位置をリセットする

ページ遷移ということになると、スクロール位置は一番上にリセットされてほしいですよね。 それを実現するために、React RouterのScrollRestorationコンポーネントを使用します。

ScrollRestorationはReact Routerのv6.4.0で導入されたコンポーネントです。

locationの変更時にスクロール位置を変更するコンポーネントとのこと。

This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains.

アプリケーションのroot routeでレンダリングすべきと書かれています。

You should only render one of these and it's recommended you render it in the root route of your app

routerの宣言を以下のように変更します。

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      { path: "/", element: <Home /> },
      { path: "next", element: <Next /> },
    ],
  },
]);

function Root() {
  return (
    <>
      <ScrollRestoration />
      <Outlet />
    </>
  );
}

ルートURLではRootコンポーネントレンダリングされ、ScrollRestorationコンポーネントとURLに合わせたページ要素が並列にレンダリングされるようにしました。 ( routerにおけるchildrenはOutletコンポーネント部分にレンダリングされます)

この変更により、アプリケーションの挙動は以下のようになります。

ページ遷移時にスクロール位置が一番上にリセットするようになりました。

スクロール位置リセットの抑止

ScrollRestorationでアプリケーション全体にスクロールリセットを導入した状態でも、クリックでURL変更を行うLinkコンポーネントのpropsで一部のページ遷移ではスクロール位置のリセットを抑止できます。

<Link preventScrollReset={true} />

前ページのスクロール位置の記憶

アプリ内で以前訪れたページを再訪した際に以前のスクロール位置にいてほしいことありますよね。 ブラウザバックで前のページに戻る際はスクロール位置が復元されていますが、ナビゲーションメニューで以前のページを再訪する場合はデフォルトでは復元されません。

ScrollRestorationコンポーネントのgetKey propsを以下のように設定すると、ナビゲーションでの再訪時にもスクロール位置を復元できます。

<ScrollRestoration
  getKey={(location) => {
    return location.pathname;
  }}
/>

HomeからNextへの遷移は初訪問なのでスクロール位置が一番上になりますが、NextからHomeへの遷移時は再訪になるため、スクロール位置が下部になっていることが確認できました。

まとめ

今回は、React Routerにおけるページ遷移時のスクロール位置をScrollRestorationコンポーネントを用いて制御する方法を紹介しました。

以前、React SPAでページネーターを実装した際にスクロール位置をリセットすることができなかった経験があるので、このコンポーネントを用いてスクロール位置をリセットできるページネーターを作ってみたいです。

21日目は、@comi19さんで「社内で喋ったことあるやつ」です。Don't miss it...