beforeunload
beforeunloadはページがアンロードされようとするときにwindow
で発生するイベントです。
イベントのpreventDefault()
が呼び出されるようなイベントハンドラを登録することで、ページがアンロードされるタイミングで以下のように確認ダイアログを表示させられます(画像はページをリロードしようとした時のChromeにおける表示)。
上記のような確認ダイアログを出すために以下のように登録しました。
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
e.returnValue = '';
})
ChromeではpreventDefault
を呼び出すだけではなく、イベントのreturnValue
に文字列を詰め込む必要があります。
他のブラウザでもpreventDefault
を呼び出すだけではなく、イベントのreturnValue
に文字列を詰め込むもしくは、文字列をイベントハンドラの返り値として渡さなければ確認ダイアログが表示されないような仕様であることが多いです。
useBeforeUnload
ReactではReact外のシステムと連携するときはエフェクトを利用して実装する必要があります。window.addEventListner
もその例の1つで、一般的に以下のような実装となります。
useEffect(() => {
window.addEventListener(eventName, handler);
return () => {
window.removeEventListener(eventName, handler);
}
}, []);
React Routerはこのような実装を短縮してbeforeunload
にイベントハンドラを登録するときのヘルパーとしてuseBeforeUnload
APIを持ちます。ページをアンロードしたときに確認ダイアログを出したい場合は以下のように記述します。
useBeforeUnload(
useCallback((e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = '';
}, [state]),
{ capture: true }
);
第1引数は必須で登録するイベントハンドラを記述します。このハンドラはuseCallback
によって包み込む必要があります。
第2引数は任意で振る舞いを指定するオプションを渡します。オプションはオブジェクトでcapture
をキーとして持ちます。capture
は真偽値で、true
の場合はキャプチャリングフェーズで発火して、false
の場合はバブリングフェーズで発火します。
デフォルトはfalse
なのでtrue
に設定したい場合に第2引数を指定するような使い方です。
// captureがtrueの場合
useBeforeUnload(
useCallback((e: BeforeUnloadEvent) => {
console.log(1);
}, [state]),
{ capture: true }
);
// captureがfalse(デフォルト)の場合
useBeforeUnload(
useCallback((e: BeforeUnloadEvent) => {
console.log(2);
}, [state])
);
実装は以下のようになっています。
export function useBeforeUnload(
callback: (event: BeforeUnloadEvent) => any,
options?: { capture?: boolean }
): void {
let { capture } = options || {};
React.useEffect(() => {
let opts = capture != null ? { capture } : undefined;
window.addEventListener("beforeunload", callback, opts);
return () => {
window.removeEventListener("beforeunload", callback, opts);
};
}, [callback, capture]);
}
エフェクトを利用した標準的な書き方になっています。イベントハンドラをuseCallback
で包み込む必要があったのはエフェクト内で利用するからということがわかります。
このようにAPIとして切り出すことでイベントハンドラを登録するときのエフェクトによる複雑さを隠すことができるのでReact Routerを使わない時に同様のAPIを定義したり、イベントハンドラの登録を一般化したAPIとして切り出して実装すると良いと感じました。
export function useEventListener<K extends keyof WindowEventMap>(
eventName: T,
callback: (event: WindowEventMap[T]) => void,
options?: boolean | AddEventListenerOptions,
): void {
useEffect(() => {
window.addEventListener(eventName, callback, options);
return () => {
windowremoveEventListener(eventName, callback, options);
};
}, [eventName, handler, options]);
}