理系学生日記

おまえはいつまで学生気分なのか

SPAで履歴を遷移するときのウィンドウ位置

いわゆるブラウザの「戻る」ボタンをクリックして画面を遷移すると、前画面のウィンドウ位置を保持するのが通常です。例えばとあるサイトに一覧画面があるとして、ブラウザを大きくスクロールして特定のレコードを表示し、そのレコードをクリックします。すると当該のレコードの詳細画面に遷移します。ここで「戻る」ボタンを押すと一覧画面に戻り、特定レコードが表示されているスクロール位置に戻る、という挙動です。

この挙動はSPA(Single Page Application)でも同様に期待されます。SPAでは画面遷移が行われても、ブラウザの履歴を操作することで前画面に戻ることができます。しかし、そううまくいかないのがSPAの難しいところです。

SPAでもブラウザはスクロール位置を復元しようとするが、そもそもいつ戻った画面が表示し終わったのかがわからないため、画面構築中にスクロールしてしまったりということが起こる。また、スクロールしようとしても、戻った時に該当のDOMがある保証もない。制御する方法も、History APIではhistory.scrollRestorationに"auto" を指定してブラウザに任せるか、 "manual"を指定してOpt-Outするしかなかった。

Navigation API による「JS での画面遷移」と SPA の改善

history.scrollRestoration

そもそもhistory.scrollRestorationが何かというと、ブラウザがスクロール位置を復元する挙動を制御するプロパティです。autoを指定するとブラウザがスクロール位置を復元します。manualを指定すると、スクロール位置の復元をブラウザに任せず、自分で制御できます。今僕はここで偉そうに書いていますが、このプロパティ知りませんでした。

scrollRestoration は History インターフェイスのプロパティで、ウェブアプリケーションが履歴の移動の動作で既定のスクロール位置の復元を明示的に設定できるようにします。

History: scrollRestoration プロパティ - Web API | MDN

具体的なデモは以下のサイトで確認できます。

ただ、このプロパティをmanualに設定しても、SPAの画面遷移時にスクロール位置を復元できない時があるというのは前述の引用のとおりです。

React Routerの解法

ではSPAでスクロール位置を復元するにはどうすればいいのでしょうか。React Routerには、このためにScrollRestorationというコンポーネントが用意されています。

実装は面白くて、ざっくりいうと次のように制御します。

  1. まずhistory.scrollRestorationmanualに設定し、スクロール位置の復元責任をブラウザではなくSPAに持たせる
  2. pagehideイベントを監視し、画面遷移タイミングで、その時のスクロール位置をsessionStorageへ記録する
  3. sessionStorageからスクロール位置を取得し、window.scrollToでスクロール位置を復元する

これは、Next.jsでもおよそ同様のようです。

色々とケアされた実装になっており、これを自力で実装するのは正直しんどいなという印象を持ちました。React Routerを使っている場合は、このScrollRestorationコンポーネントを使うのが良いでしょう。

古いReact Router使っているんだけど

古いReact Router (v6.4未満)を使っている場合は、ScrollRestorationコンポーネントが使えません。その辺りの緩和方法は以下に載っています。

ここで紹介されている方法は、画面遷移は強引にwindow.scrollTo(0, 0)でトップに戻す、というものです。UXは悪いですが、スクロール位置の復元が難しい場合はこれでしのぐのも1つの方法なのでしょう。

import { useEffect } from "react";
import { useLocation } from "react-router-dom";

export default function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}