😖

Next.js で redirect 直後に勝手にスクロールする問題

に公開

概要

ネストの親ページから子ページにリダイレクトするという設計のページにおいて、リダイレクト後に勝手にページがスクロールされてしまうという問題に直面しました。
調べても解決できなかったのですが、こういうこともあるんだという紹介のため(&解決方法をご存じの方がいらっしゃることに望みをかけて)記事にします。

環境

Next.js (App Directory) v14.2.4

どういう問題か

問題の再現

問題の様子が下記です。(画質が悪くてすみません!)

トップページのリンクをクリックし、別のページに遷移した直後にページがスクロールしましたが、私はスクロールしていません。
勝手にスクロールが行われました🥺

(実際に動かせる再現コード を用意いたしましたので、動かしてみたい方は clone してご利用ください。)

問題が再現する条件

ミニマムな再現条件として、ファイル構成を以下のように仮定します。

src
|-app
  |-layout.tsx
  |-page.tsx
  |-scroll
    |-layout.tsx
    |-page.tsx
    |-child
      |-page.tsx

トップページに /scroll/scroll/child の子ページで計三ページです。

トップページには /scroll ページへのリンクだけがあります。

src/app/page.tsx
import Link from "next/link";

export default function HomePage() {
  return (
    <div>
      <Link href="/scroll">/scroll への next/link のリンク</Link>
    </div>
  );
}

/scroll ディレクトリは Layout を共有しています。
Layout の内容は以下の通りです。

src/app/scroll/layout.tsx
type Props = {
  children: React.ReactNode;
};
export default function Layout({ children }: Props) {
  return (
    <section>
      <h1>親ページの内容</h1>
      {children}
    </section>
  );
}

そして /scroll のページは /scroll/child へリダイレクトするのみです。

src/app/scroll/page.tsx
import { redirect } from "next/navigation";

export default function ParentPage() {
  redirect("/scroll/child");
}

/scroll/child の中身は適当に。

src/app/scroll/child/page.tsx
export default function ChildPage() {
  return (
    <section>
      <h2>子ページの内容</h2>
    </section>
  );
}

以上の構成において、次の条件を満たすと問題が発生します。

  • トップページなど、/scroll 以外のページから /scroll への next/link のリンクを経由して /scroll にアクセスし、/scroll/child へリダイレクトする
  • /scroll/child に到達したときに /scroll/child 固有の内容を表示するにはページをスクロールする必要がある

/scroll/child へリダイレクト後、/scroll/child 固有の内容が表示できる部分まで勝手にスクロールします。

なぜスクロールするのか?

ドキュメントを調べたのですが、残念ながら本挙動を説明する記述を見つけることができませんでした……😖

Layout の説明として、遷移後のページと共通のレイアウト部分は re-render されないとは書いてあります。
しかし共通部分をスクロールで飛ばすとは書いていませんし、その挙動を制御するようなパラメータも見つけることができませんでした。

ブラウザのアドレスバーに直接 http://localhost:3000/scroll と打ち込んで画面遷移した場合はスクロールは起こらないので、redirect 自体には原因は無いと思います。

next/link で Scroll restoration を無効にしてみましたが関係ありませんでした。

回避策

現状、私が思いついて実行してみた限りでは以下のような回避策が考えられます。

  • /scroll/child に着地した直後にページトップへスクロールし、Next.js の挙動を打ち消す
  • Layout を使わない

私は最終的に Layout を使わない方針で対応しました。

しかし Layout にはページ間で状態を保持するという重要な特性があるため、その特性を必要とする場合は工夫が必要となります。
実際に私も一部の状態は保持しなければならなかったので、Context で愚直に状態を管理し、ページ遷移先で復元する処理を書くことになりました。
それでも re-render されてしまうので、表示に一瞬ちらつきが発生する等の新たな問題も起こります。

終わりに

スクロールしないようにする方法があるならぜひ Layout を使いたいので、解決方法をご存じの方がいらっしゃったら教えてください🙏

Discussion