概要
next.jsのApp RouterやLinkのデフォルト仕様として、画面の一番上に自動的にスクロールされる。それはとても望ましいが、たまにそうならない時があるので、この記事ではそれのあらゆる原因や背後の仕組みを検討する。
結論
遷移後の画面にコンテンツの描画が遅延されたことはほとんどの原因である。それを防ぐには、遅延しないか、遅延するが自力でスクロールさせるか、のどちらだと思う。
遅延描画とは
- useStateなどでフラグを持って、初期がfalse
- falseなら、コンテンツを描画せず、nullを返す
- useEffectでdidMountを検知したら、フラグをtrueに更新して、本来のコンテンツが描画される
next.jsの自動スクロールの仕組み
画面遷移が成功後を検知したら、scroll処理を行なうだけだと推測される
- app routerはページ遷移イベントを検知できないため、usePathnameでpathnameの変化を検知してやるではないかと思う
- それを検知して、スクロールしようとすると、画面コンテンツがないと、スクロール失敗になる
- その後、コンテンツが描画されて、スクロール位置が前画面のままになり、効果的には一番上にスクロールされなかった
SPAのスクロール位置
- SinglePageApplicationはブラウザに対しては物理的なロードは最初の一回だけである
- その後はJavaScriptでDOM内容を入れ替わることで効果的に画面遷移のように見えるが、実は遷移してない
- ブラウザのアドレスバーのURLが変わらないと気持ち悪いので、App Routerでそれを変わるように弄ってる
- 初期ロードが物理遷移と呼ぶなら、SPA画面遷移を論理遷移と呼んで良さそう
なので、ブラウザが持っているスクロール位置がプログラミングで操作しない限り変わらないはず。next.jsで望まれる形に画面遷移されたら、一番上にスクロールさせる。
一番上にスクロールされないあらゆるケースと対策
- 上記のような描画遅延があった場合
- 不要な遅延をなくす
- どうしても遅延しないできない場合は自力でスクロールさせる
- 画面遷移後にAPI通信が発生し、それのレスポンス内容によって描画内容が大きく変わる場合
- しようがないので、API通信成功後に自力でスクロールさせる
ブラウザのスクロール履歴機能(Scroll Restoration)
ブラウザのナビゲーションのBack/Forwardボタンで前後の画面に遷移する場合、ブラウザが勝手に元のスクロール位置に戻れる機能である。ただ、それは物理遷移の場合に動くが、SPAのような論理遷移が効かない。
next.jsの自動スクロールONの問題点
次へボタンをクリックして、次の画面が表示されたら上から順番に見たいので一番上にスクロールされるのがありがたい。
ただし、画面上の戻るボタンをクリックして前画面に戻った時も、一番上にスクロールされるのがちょっと違うかも。なぜなら、前画面を遷移前の元場所に戻られて、次へボタンをすぐクリックできるのも望まれることがあるではないか。
- 戻るボタンのクリックで
router.back()
を呼び出した場合、確かに一番上にスクロールしないが、元の場所に戻すこともない、つまり現状維持 - 戻るボタンのクリックで
router.push('/hoge')
形式で前画面に戻った場合、一番上にスクロールしてしまう- もちろん
router.push('/hoge', { scroll: false })
を呼べば、一番上にスクロールしないが、元の場所に戻すこともない、現状維持
- もちろん
Back・Forward時のスクロール位置が現状位置の問題点
- 面白いことが、縦長の画面Bの一番下で戻るボタンをクリックして、縦のサイズがより小さい画面Aに戻ったら、画面Aのスクロール場所も一番したになる
- 現状維持でスクロール位置を変えてないが、ブラウザが賢く大きい値を実際の画面サイズに修正してくれる
- 逆に縦のサイズが小さい画面Aの一番下からサイズが大きい画面Bに戻ったら、画面Bのスクロール位置が真ん中になる
つまり、プログラミングで何もしない限り、Back・Forwardする場合はスクロールの縦位置が元のままで中途半端の状態になる。
Back・Forward時にもスクロール位置を復元させるには
では、ブラウザのScroll Restoration機能のようにBack・Forward時にも賢く元の位置に戻せるにはどうすれば良いでしょうか。自力でやるしかないが、
- 画面ごとのスクロール位置を覚える
- pathnameが変わったら、必要なら元の位置に復元させる
- 元の位置がある、かつ復元の指示があればやる
実際にやってないが、しかも困難であるが、その時のアプローチだけをメモする。
- page routerと違ってapp routerでは画面遷移のイベントを拾えない
- 復元するかの指示はどうやって指示するか
画面遷移直前にスクロール位置を覚えるアプローチ
- intervalで問い合わせる
- シンプルだが、カッコよくない
- 完全に正しい位置を保証できない
-
next-navigation-guardような仕組みで遷移直前を検知する
- 正しく動くかは要検証
- より確実
- 復元に関連するが、routerやlinkをラップして、それを利用する
- routerのpushやlinkのonclickなどで位置を覚える
- より確実
- 自力でrouterやlinkを拡張する必要がある
- routerやlinkを使ってないのはブラウザのナビボタン(Back・Forward)と想定
- 戻る時も確実に指示できる
- 現実的にこのアプローチ
スクロール位置を回復アプローチ
- routerやlink拡張以外の方法では位置を覚えても、復元するかの指示が難かしいので、それらのアプローチを除外する
- ブラウザのナビボタンをクリックする場合は、常に元の位置を復元させる
- 画面操作での画面遷移は、pushやbackやlinkのonclickで処理する
- 復元しないと指示した場合は、該当ページの位置情報を削除
- 遷移後に位置情報があればそれで復元する、なければ一番上にスクロールさせる
まとめ
- スクロールされない原因はほぼコンテンツ描画の遅延のせいである
- SPAのスクロール位置がブラウザのScroll履歴が効かない
- 完璧なBack・Forwardでの位置復元をやるにはかなり労力かかるが、不可能ではない