いわゆるブラウザの「戻る」ボタンをクリックして画面を遷移すると、前画面のウィンドウ位置を保持するのが通常です。例えばとあるサイトに一覧画面があるとして、ブラウザを大きくスクロールして特定のレコードを表示し、そのレコードをクリックします。すると当該のレコードの詳細画面に遷移します。ここで「戻る」ボタンを押すと一覧画面に戻り、特定レコードが表示されているスクロール位置に戻る、という挙動です。
この挙動はSPA(Single Page Application)でも同様に期待されます。SPAでは画面遷移が行われても、ブラウザの履歴を操作することで前画面に戻ることができます。しかし、そううまくいかないのがSPAの難しいところです。
SPAでもブラウザはスクロール位置を復元しようとするが、そもそもいつ戻った画面が表示し終わったのかがわからないため、画面構築中にスクロールしてしまったりということが起こる。また、スクロールしようとしても、戻った時に該当のDOMがある保証もない。制御する方法も、History APIではhistory.scrollRestorationに"auto" を指定してブラウザに任せるか、 "manual"を指定してOpt-Outするしかなかった。
history.scrollRestoration
そもそもhistory.scrollRestoration
が何かというと、ブラウザがスクロール位置を復元する挙動を制御するプロパティです。auto
を指定するとブラウザがスクロール位置を復元します。manual
を指定すると、スクロール位置の復元をブラウザに任せず、自分で制御できます。今僕はここで偉そうに書いていますが、このプロパティ知りませんでした。
scrollRestoration は History インターフェイスのプロパティで、ウェブアプリケーションが履歴の移動の動作で既定のスクロール位置の復元を明示的に設定できるようにします。
具体的なデモは以下のサイトで確認できます。
ただ、このプロパティをmanual
に設定しても、SPAの画面遷移時にスクロール位置を復元できない時があるというのは前述の引用のとおりです。
React Routerの解法
ではSPAでスクロール位置を復元するにはどうすればいいのでしょうか。React Routerには、このためにScrollRestoration
というコンポーネントが用意されています。
- ドキュメント:ScrollRestoration v6.23.0 | React Router
- 実装:react-router/packages/react-router-dom/index.tsx at 2c8d437bde74c6f3ddacbf4417bbf54d5b851c16 · remix-run/react-router · GitHub
実装は面白くて、ざっくりいうと次のように制御します。
- まず
history.scrollRestoration
をmanual
に設定し、スクロール位置の復元責任をブラウザではなくSPAに持たせる pagehide
イベントを監視し、画面遷移タイミングで、その時のスクロール位置をsessionStorageへ記録する- 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; }