【Next.js】ページ遷移時に入力内容破棄の確認モーダルを表示する便利なカスタムフック作ってみた。

# はじめに
実務を経験する上で、
「入力フォームなどがあるページで入力途中の内容がある場合に、
ユーザーがページ遷移をしようとした時に入力内容の破棄をアラートする。」
といった対応が必要なケースがあると思います。

そういったケースで、
簡単に使えるカスタムフックスを作成しました。

## 使用技術に関して
主にこのフックス内で使用しているパッケージとそのバージョンは以下です。
`Next.js: ^12.1.7-canary.41`
`react: 18.2.0`
`@mantine/core: 4.2.9`
`@mantine/hooks: 4.2.9`
`tailwindcss: ^3.1.3`

# 目次
[1.コードはこんな感じ](#1-コードはこんな感じ)
[2.それでは解説](#2-それでは解説)
[3.終わりに](#3-終わりに)

# 1. コードはこんな感じ
``` JSX:usePreventLeavePage.tsx
import { Modal } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Button } from "src/components/Button";

/**
 * @package
 */
export const usePreventLeavePage = <T,>(initialValues: T, values: T, isRequestDone: boolean) => {
  const router = useRouter();
  const [isPreventModalOpen, preventModalHandler] = useDisclosure(false);
  const [nextUrl, setNextUrl] = useState<string | undefined>(undefined);

  const renderModal = () => {
    return (
      <Modal
        opened={isPreventModalOpen}
        onClose={() => {
          throw "Abort route";
        }}
        withCloseButton={false}
        closeOnClickOutside={false}
      >
        <div>
          <p>編集内容がリセットされます、本当にページ遷移しますか？</p>
          <div className="flex gap-2 justify-end">
            <Button
              className="w-[120px]"
              color="gray"
              onClick={() => {
                setNextUrl(undefined);
                preventModalHandler.close();
                throw "Abort route";
              }}
            >
              しない
            </Button>
            <Button
              className="w-[120px]"
              onClick={() => {
                if (nextUrl) {
                  router.push(nextUrl);
                  return;
                }
                router.reload();
              }}
            >
              する
            </Button>
          </div>
        </div>
      </Modal>
    );
  };

  useEffect(() => {
    const pageChangeHandler = (url: string) => {
      setNextUrl(url);
      if (
        !isRequestDone &&
        !nextUrl &&
        JSON.stringify(initialValues) !== JSON.stringify(values)
      ) {
        preventModalHandler.open();
        throw "Abort route";
      }
    };

    router.events.on("routeChangeStart", pageChangeHandler);
    return () => {
      router.events.off("routeChangeStart", pageChangeHandler);
    };
  }, [initialValues, nextUrl, preventModalHandler, router.events, values]);

  return { renderModal };
};
```

呼び出す時は以下
```jsx
  const { renderModal } = usePreventLeavePage(initialValues, values, isRequestDone);
```

# 2. それでは解説

```JSX
export const usePreventLeavePage = <T,>(initialValues: T, values: T, isRequestDone: boolean) => {
```

入力破棄の対象となるフォームの値の`初期値(initialValues)`と`動的に変化する値(values)`、`リクエスト後のフラグ(isRequestDone)`をそれぞれpropsとして受け渡す。
Tには上記のように記述することで、その値の型が勝手に入るようにしています。


```jsx
  const [isPreventModalOpen, preventModalHandler] = useDisclosure(false);
```
上記に関してはMantineの提供するフックスなので、詳しくは以下をご覧ください。

https://mantine.dev/hooks/use-disclosure/

```jsx
  const [nextUrl, setNextUrl] = useState<string | undefined>(undefined);
  // ...
        <div>
          <p>編集内容がリセットされます、本当にページ遷移しますか？</p>
          <div className="flex gap-2 justify-end">
            <Button
              className="w-[120px]"
              color="gray"
              onClick={() => {
                setNextUrl(undefined);
                preventModalHandler.close();
                throw "Abort route";
              }}
            >
              しない
            </Button>
            <Button
              className="w-[120px]"
              onClick={() => {
                if (nextUrl) {
                  router.push(nextUrl);
                  return;
                }
                router.reload();
              }}
            >
              する
            </Button>
          </div>
        </div>
  // ...
  useEffect(() => {
    const pageChangeHandler = (url: string) => {
      setNextUrl(url);
      if (
        !isRequestDone &&
        !nextUrl &&
        JSON.stringify(initialValues) !== JSON.stringify(values)
      ) {
        preventModalHandler.open();
        throw "Abort route";
      }
    };

    router.events.on("routeChangeStart", pageChangeHandler);
    return () => {
      router.events.off("routeChangeStart", pageChangeHandler);
    };
  }, [initialValues, nextUrl, preventModalHandler, router.events, values, isRequestDone]);
```

`nextUrl`にはユーザーが**本来の遷移するはずだった遷移先**のURLを代入します。

`pageChangeHandler`はページの遷移を感知した時に発火する関数です。
引数として遷移先のURLを受け取るので、それを`nextUrl`にセットしておきます。

そして条件分岐についてです。
まず`isRequestDone`は**フォーム送信後にページ遷移を行う場合にブロックしてしまわないように**送信前のみにブロックするようにします。

`JSON.stringify(initialValues) !== JSON.stringify(values)`で値に変化がないか確認します。もしもパラメータの並びに整合性がない場合は、並べ替えの関数を別に作る方が正確に動くと思います！
自分の場合はそもそも`initialValues`は`values`の初期値のコピーとして定義しているので、上記のような等式でも正確に動きます。

あとは`useEffect`内の
`router.events.on("routeChangeStart", pageChangeHandler);`の部分ですが、
こちらは`routeChangeStart`という遷移前のイベントに対して関数を実行している形です。

# 3. 終わりに
以上、入力破棄の確認モーダル用のカスタムフックスの説明は終わりです。

しかし現状、リロード時に対応できていなかったり、その他にも改善点が見つかるかもです。
なので随時ブラッシュアップし、この記事の内容を更新していきます。

それでは、ここまで読んでいただきありがとうございました！！！