🐜

Next.js ブラウザバック・ページ遷移前にconfirmを表示したい

2024/03/28に公開

概要

会員登録フォームなどの入力中に、誤ってブラウザバックすることがある。
入力内容が消えてしまい、離脱につながる。
上記を防ぎたい。


前提

next@13.5.4 の Pages Router

対象読者

  • Next.jsで画面制御をしたい人
  • hooksについて理解を深めたい人
  • useEffectの理解を深めたい人

いいね!してね

この記事の事例は必要に応じて今後追記していく予定です!
「新しい事例が知りたい」「他の事例も知りたい」と思った人は、ぜひこの記事にいいね👍してください。筆者のモチベーションにつながります!

それでは以下が本編です。

説明すること/説明しないこと

説明すること

  • useEffectでconfirmを出す方法
  • routerの挙動を止めようとしたが失敗した方法

useEffectでconfirmを出す方法

component

export const Posts: FC<{}> = () => {
  const router = useRouter();

  useConfirmRouteNavigation(async (url: string) => {
    // 同じパスに遷移しようとした場合は遷移をキャンセルする
    const nextPath = "/posts";
    if (url === nextPath) {
      return false;
    }

    const answer = window.confirm("このサイトを離れますか?");

    if (!answer) {
      // 現在のページへ遷移させる
      return await router.push(router.asPath);
    }
    return false;
  });

  ...省略
}

hooks

import { useRouter } from "next/router";
import { useEffect, useRef } from "react";

type OnRouteChangeFunction = (url: string) => Promise<boolean>;

export const useConfirmRouteNavigation = (fn: OnRouteChangeFunction) => {
  const ref = useRef(fn);
  useEffect(() => {
    // 初期描画時のrefに関数をセットする
    ref.current = fn;
  }, [fn]);

  // ブラウザバック・外部サイトへの遷移の場合
  useEffect(() => {
    const nativeBrowserHandler = (event: BeforeUnloadEvent) => {
      event.preventDefault();
      event.returnValue = "";
    };

    window.addEventListener("beforeunload", nativeBrowserHandler);

    return () => {
      window.removeEventListener("beforeunload", nativeBrowserHandler);
    };
    // ページ遷移時にcleanupするためにrefを渡す
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ref]);

  const router = useRouter();
  // サイト内ページ遷移の場合
  useEffect(() => {
    const onRouteChangeStart = (url: string) => fn(url);

    router.events.on("routeChangeStart", onRouteChangeStart);

    return () => {
      router.events.off("routeChangeStart", onRouteChangeStart);
    };
    // ページ遷移時にcleanupするためにrefを渡す
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ref]);
};

routerの挙動を止めようとしたが失敗した方法

component側でエラーをthrowする + hooks側でrouteChangeErrorをemitすると、
console.errorは消えた。

が、router.pushが止まらなくなった(ページ遷移してしまった)

  useConfirmRouteNavigation(async (url: string) => {
    // 同じパスに遷移しようとした場合は遷移をキャンセルする
    const nextPath = "/posts";
    if (url === nextPath) {
      return false;
    }

    const answer = window.confirm("このサイトを離れますか?");

    if (!answer) {
      // エラーを発生させてページ遷移をキャンセルさせたい
      throw new Error("abort");
    }
    return false;
  });
  const router = useRouter();
  // サイト内ページ遷移の場合
  useEffect(() => {
    const onRouteChangeStart = async (url: string) => {
      try {
        await fn(url);
      } catch (error) {
        if (error.message === "abort") {
          router.events.emit("routeChangeError", error);
        } else {
          throw error;
        }
      }
    };

    router.events.on("routeChangeStart", onRouteChangeStart);

    return () => {
      router.events.off("routeChangeStart", onRouteChangeStart);
    };
    // ページ遷移時にcleanupするためにrefを渡す
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ref]);

まとめ

多くのウェブサイトで需要がありそうな機能だが、実装方法がいまだに確立されていないのか?(or 私が知らないだけ、、?)

知識を深めるために試行錯誤したが自力で解消できなかった。
つよつよエンジニアからのコメントを待ちます!
同様の機能を実装したライブラリの実装方法をみれば何かわかるかもしれない(TODO)

Discussion