🛡️

Next.js App Router でのフォーム離脱防止

2023/12/25に公開

概要

フォームの入力中に離脱すると入力中の内容が失われてしまいます。離脱前に確認ダイヤログを挟むことで意図せぬ入力内容の消失を防ぐことができます。

離脱には以下の 2 種類があります。

  1. ブラウザ操作による離脱: タブを閉じる、更新する、ブラウザナビゲーション(戻る、進む)
  2. Next.js のルーティングによる離脱: Next.js の機能で別ルートに遷移する

このうち 2 は Next.js の router.events という機能でイベントを検知&キャンセルすることになりますが、この機能が App Router から使えなくなりました

現在フィードバックが集まっていますがコントリビューターから明確な回答が得られておらず、この機能が復活するのかも不明な状況です。

2024年5月8日追記:
Lee Robinson 氏より正式な回答が得られましたが依然ハッキーであるため、よりスマートな解決策が期待されます。LeeRob 氏のアプローチはだいぶ複雑なので個人的には引き続き当記事のアプローチを採用していきます。

解決策

代替案として、Web API のイベントリスナーを使ってリンクのクリックを検知&確認&キャンセルします。なお、nprogress も同様のアプローチでこの問題を解決しているようです。

まずガード用のカスタムフックを作成します。

import { useEffect } from 'react';

export const useFormGuard = (isDirty: boolean) => {
  useEffect(() => {
    const handleClick = (event: MouseEvent) => {
      if (
        isDirty &&
        event.target instanceof Element &&
        event.target.closest('a:not([target="_blank"]')
      ) {
        if (!window.confirm('ページを離れても良いですか?')) {
          event.preventDefault();
          event.stopPropagation();
        }
      }
    };

    const handleBeforeUnload = (event: BeforeUnloadEvent) => {
      if (isDirty) {
        event.preventDefault();
        return (event.returnValue = '');
      }
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    window.addEventListener('click', handleClick, true);

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
      window.removeEventListener('click', handleClick, true);
    };
  }, [isDirty]);
};

以下のようにカスタムフックを使います。これを差し込んだページではフォームガードが有効になります。確認を挟むべきかどうかのフラグを渡すだけなので、 必ずしも react-hook-form を使う必要はありません。以下は  React Hook Form を使った例です。

export default function FormPage() {
  const { formState: { isDirty } } = useForm();

  useFormGuard(isDirty);
  // ...
}

動作デモ

https://dninomiya.com/ja/docs/form

補足

  • Safari では beforeunload がブラウザバックに反応しないのでブラウザバックは離脱ガードできません(防ぎ方あれば教えてください)
  • beforeunload の際の確認メッセージはほとんどのモダンブラウザで編集不可能です
  • このアプローチでは JS で router.push した場合の離脱防止ができません。そこも考慮する場合、将来的な router.events の復活を待つか、 router.push の現場に欠かさず離脱防止処理を差し込む必要があります。

router.events に関するアップデートは以下のディスカッションで追えます。

https://github.com/vercel/next.js/discussions/41934

その他のアプローチ

location-state を使うアプローチもあるようです。
https://zenn.dev/akfm/articles/location-state

その他にも良さげなアプローチがあればコメントでいただければ幸いです。

宣伝

𝕏 アカウント
Web アプリ開発に関する Tips を日々発信しています。

Discussion