Next.js App Router でのフォーム離脱防止
概要
フォームの入力中に離脱すると入力中の内容が失われてしまいます。離脱前に確認ダイヤログを挟むことで意図せぬ入力内容の消失を防ぐことができます。
離脱には以下の 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);
// ...
}
動作デモ
補足
- Safari では
beforeunload
がブラウザバックに反応しないのでブラウザバックは離脱ガードできません(防ぎ方あれば教えてください) -
beforeunload
の際の確認メッセージはほとんどのモダンブラウザで編集不可能です - このアプローチでは JS で
router.push
した場合の離脱防止ができません。そこも考慮する場合、将来的なrouter.events
の復活を待つか、router.push
の現場に欠かさず離脱防止処理を差し込む必要があります。
router.events
に関するアップデートは以下のディスカッションで追えます。
その他のアプローチ
location-state を使うアプローチもあるようです。
その他にも良さげなアプローチがあればコメントでいただければ幸いです。
宣伝
𝕏 アカウント
Web アプリ開発に関する Tips を日々発信しています。
Discussion