わたすけです。
Next.jsで、こんな感じのウィンドウを出したいと思ったことは有りませんか?
よく見るやつですね。ページ移動時にこれを表示する方法はちらほら見受けられるのですが、例えば「フォームの内容が更新されている」という状態をstateで管理していて、それをもとに確認ウィンドウの表示・非表示を切り替えるのは少しややこしい方法が必要です。
ということで、今回はやり方を紹介しようと思います。
環境
React 17.0.2 + Next.js 12.0.7、TypeScriptで解説をします。あとFunction Componentでのやり方です(Class Componentでもなんとかなると思います)。
前提
useStateでboolean値を管理します。
const [edited, SetEdited] = React.useState(false);
useEffect等を使って、何かしらの編集が行われたらeditedがtrueになるようにしておきます。
さて、そもそもNext.jsでページ移動時に確認を表示する方法は、以下の2つです。
- Event Listener
- Router Events
というか、完全に対応するためにはどちらも使わなければいけません。
まず、EventListenerの方はNextに限らない方法で、
const Confirm = (e: BeforeUnloadEvent): void => {
e.preventDefault();
e.returnValue = '変更は破棄されます。ページを移動してもよろしいですか?';
};
window.addEventListener('beforeunload', Confirm);
こんな感じでOKです。ただし、Next.jsの場合、この関数はリロード時のみ呼ばれます。つまり、router.push等の関数でページを移動する場合、何も警告が出ないということになります。
これを防ぐために使うのがRouter Eventsです。
const RouterEvent = () => {
if (!window.confirm('変更は破棄されます。ページを移動してもよろしいですか?')) {
throw 'canceled';
}
};
const router = useRouter();
router.evens.on('routeChangeStart', RouterEvent);
windows.confirmでキャンセルが押されたらthrowによってRouterのイベントを潰している感じです。
問題点・対策
さて、ここで先程の「editedがtrueでなければ表示しない」という要件を満たそうとする時、真っ先に挙がるのは「if (!edited) return;
を関数の一番上に書く」でしょう。
ただし。これでは動かないはずです。console.logとかを使えばわかりますが、stateが更新されてもページ移動時には無関係になります(初期値が採用されてるらしい?あんまり詳しく見てないです)
ということで、これを解決するためには、以下の手順が必要です。
useCallbackを使う
関数をuseCallbackで囲います。
const hoge = React.useCallback(() => ~, []);
stateではなくrefを使う
useRefを用いてrefを作成し、ref.currentで参照します。
const edited = React.useRef(false);
ということで、完成形はこのような感じになります。
const Confirm = React.useCallback((e: BeforeUnloadEvent): void => {
if (!edited.current) return;
e.preventDefault();
e.returnValue = '変更は破棄されます。ページを移動してもよろしいですか?';
}, []);
window.addEventListener('beforeunload', Confirm);
カスタムフック化するとなお使いやすいと思います。実装はこんな感じになります。
おわりに
ということで、Next.jsで確認を表示する方法でした。Reactはまだ知らないことも多いため、がんばっていこうとおもいます。
コメント