6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React RouterのuseBeforeUnload

Posted at

beforeunload

beforeunloadはページがアンロードされようとするときにwindowで発生するイベントです。
イベントのpreventDefault()が呼び出されるようなイベントハンドラを登録することで、ページがアンロードされるタイミングで以下のように確認ダイアログを表示させられます(画像はページをリロードしようとした時のChromeにおける表示)。
スクリーンショット 2023-11-02 11.20.22.png

上記のような確認ダイアログを出すために以下のように登録しました。

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にイベントハンドラを登録するときのヘルパーとしてuseBeforeUnloadAPIを持ちます。ページをアンロードしたときに確認ダイアログを出したい場合は以下のように記述します。

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]);
}
6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?