🙄

Next.JSでページ遷移する前にbeforeunloadはさみたい時

2021/09/12に公開

フォームに入力中のデータがの残っていて、ページ遷移する前に注意(window.confirm())を出したくて、色々ハマりましたので、書き残しておきます。

結論から言う、beforeunloadwindow.location.hrefF5ようなページの再読み込むが発生する時には発火しますが、Next.jsのRouterのような ブラウザーのhistory APIでの画面遷移の場合は発火しないようです。

解決策をいくつか見つかったので、全部書いておきます。

解決策1

import SingletonRouter, { Router } from 'next/router'

useEffect(() => {
  SingletonRouter.router.change = (...args) => {
    if (confirm('Want to leave?')) {
      return Router.prototype.change.apply(SingletonRouter.router, args)
    } else {
      return new Promise((resolve, reject) => resolve(false))
    }
  }
  return () => {
    delete SingletonRouter.router.change
  }
}, [])

github issue にありました、https://github.com/vercel/next.js/issues/2476#issuecomment-612483261
routerchangeというプライベートメソッドを上書きする事でページ遷移をキャンセルできるみたいです、詳しいソースコードは見てませんが、JSはともかくTS使う場合は「プライベートメソッドは上書きできないぞー」とのコンパイルエラーが出ます。

解決策2

  import { useRouter } from 'next/router';

  const router = useRouter();
  
  const pageChangeHandler = () => {
    const answer = window.confirm('コメント内容がリセットされます、本当にページ遷移しますか?');
    if(!answer) {
      throw 'Abort route';
    }
  };
  
  useEffect(() => {
    router.events.on('routeChangeStart', pageChangeHandler);
    return () => {
      router.events.off('routeChangeStart', pageChangeHandler)
    };
  }, []);

同じくgithub issueから発見 https://github.com/vercel/next.js/issues/2476#issuecomment-563190607
ページ遷移する前に発火するrouteChangeStartにハンドラーを登録しておいて、ハンドラーの中でエラーを発生させて処理を止める。

結論

解決策2の方が安全な気がします、ただ、逆にNext.JS内での遷移制御だけでは、F5のような画面再読み込みの場合、発火しないので、beforeunloadも必要ですね。
つまり👇なります。

  import { useRouter } from 'next/router';

  const router = useRouter();
  
  const pageChangeHandler = () => {
    const answer = window.confirm('コメント内容がリセットされます、本当にページ遷移しますか?');
    if(!answer) {
      throw 'Abort route';
    }
  };
  
  const beforeUnloadhandler = (event) => {
    event.returnValue = 'コメント内容がリセットされます、本当にページ遷移しますか?';
  };
  
  useEffect(() => {
    router.events.on('routeChangeStart', pageChangeHandler);
    window.addEventListener('beforeunload', beforeUnloadhandler);
    return () => {
      router.events.off('routeChangeStart', pageChangeHandler)
      window.removeEventListener('beforeunload', beforeUnloadhandler);
    };
  }, []);

Discussion