はじめに
実務を経験する上で、
「入力フォームなどがあるページで入力途中の内容がある場合に、
ユーザーがページ遷移をしようとした時に入力内容の破棄をアラートする。」
といった対応が必要なケースがあると思います。
そういったケースで、
簡単に使えるカスタムフックスを作成しました。
使用技術に関して
主にこのフックス内で使用しているパッケージとそのバージョンは以下です。
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. コードはこんな感じ
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)
で値に変化がないか確認します。もしもパラメータの並びに整合性がない場合は、並べ替えの関数を別に作る方が正確に動くと思います!
自分の場合はそもそもinitialValues
はvalues
の初期値のコピーとして定義しているので、上記のような等式でも正確に動きます。
あとはuseEffect
内の
router.events.on("routeChangeStart", pageChangeHandler);
の部分ですが、
こちらはrouteChangeStart
という遷移前のイベントに対して関数を実行している形です。
3. 終わりに
以上、入力破棄の確認モーダル用のカスタムフックスの説明は終わりです。
しかし現状、リロード時に対応できていなかったり、その他にも改善点が見つかるかもです。
なので随時ブラッシュアップし、この記事の内容を更新していきます。
それでは、ここまで読んでいただきありがとうございました!!!