1
0

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.

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

Posted at

はじめに

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

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

使用技術に関して

主にこのフックス内で使用しているパッケージとそのバージョンは以下です。
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.コードはこんな感じ
2.それでは解説
3.終わりに

1. コードはこんな感じ

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 };
};

呼び出す時は以下

  const { renderModal } = usePreventLeavePage(initialValues, values, isRequestDone);

2. それでは解説

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

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

  const [isPreventModalOpen, preventModalHandler] = useDisclosure(false);

上記に関してはMantineの提供するフックスなので、詳しくは以下をご覧ください。

  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)で値に変化がないか確認します。もしもパラメータの並びに整合性がない場合は、並べ替えの関数を別に作る方が正確に動くと思います!
自分の場合はそもそもinitialValuesvaluesの初期値のコピーとして定義しているので、上記のような等式でも正確に動きます。

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

3. 終わりに

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

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

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

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?