はじめに
仕事で Next.js のページ上でページ遷移時の確認モーダルを出すコードを作成したのでその上で知ったことに関して備忘録代わりにこちらで記載しておきます。
useEffect の理解
今回の目的の達成のために、Next.js の useEffect を用いようと思います。
useEffect は引数に指定した関数の実行をコンポーネントのレンダリング後に行うことができるメソッドです。
基本的なつくりは useEffect(関数, データ配列)
となっており、第二引数の配列内のデータが更新されるたびに、第一引数の関数が実行されます。
今回の目標は「ページ遷移時の確認モーダル表示」なので、コンポーネントがアンマウントされたタイミングでの処理についても気を付ける必要があります。
その為、アンマウントされたタイミングでの処理も含めての基本的な構文は以下の様になります。
useEffect(() => {
// レンダリング時の処理
return () => {
// アンマウント時の処理
}
}, [
// 監視したいデータ
])
ページ遷移時の確認モーダルのイベント
次に、useEffect 内に記述する処理について考えますが、画面の遷移を感知するのであれば、ご想像の通りイベントの指定を行う必要があります。
今回のターゲットは「画面の遷移」ですが、Next.js 上で考える場合、以下の二種類の遷移を考える必要があります。
- アプリ内の遷移
- アプリ外の遷移、ブラウザによるリロード
アプリ内の遷移に対応するイベント指定
Next.js の基本的な流儀に則っているのであれば、アプリ内のページ遷移に関しては router オブジェクトを扱うことになります。
router オブジェクトにイベントを指定する場合、 router.events.on でイベントの指定と発火時の処理を指定します。
router.events.on で指定できるイベントはいくつか存在しますが、今回はルートの変更が開始した際に発火する routeChangeStart を指定します。
あとは、実際の発火時の処理を指定すればよいので、OK とキャンセルのボタンが付いたモーダルを出力できる window.confirm を利用して、以下の様に実装します。
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)
}
}, [])
上記の実装でさらっと書いていますが、アンマウント時の処理として、イベントの指定を外す処理を router.events.off で指定しています。
アプリ外への遷移やページのリロードに対応するイベント指定
ここまで触れてきた router オブジェクトはアプリ内の遷移のみを見ています。
そのため、アプリ外への遷移やページのリロード時のイベントの指定には普通の javascript でのイベント指定である、 window.addEventListner を使用する必要があります。
window.addEventListner で指定できるイベントのうち、今回は目標である「画面の遷移」に対応するイベントとしては beforeunload を指定します。
router.events.on と同様に、window.addEventListner でも実際の発火時の処理を指定する必要がありますが、window.confirm を利用するまでもなく確認ダイアログが表示されるので、以下の様に指定を行えば良い形になります。
const beforeUnloadhandler = (event) => {
event.returnValue = '本当にページ遷移しますか?'
}
useEffect(() => {
window.addEventListener('beforeunload', beforeUnloadhandler)
return () => {
window.removeEventListener('beforeunload', beforeUnloadhandler)
}
}, [])
event.returnValue はブラウザによって確認ダイアログに文字列を表示したりしなかったりと挙動がまちまちではありますが、何らかの指定自体は必要なので、指定しています。
最終的な実装
ここまでの実装を組み合わせて、アプリ内外を問わずにページを遷移した際に確認モーダルを出す実装は以下の通りになります。
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)
}
}, [])
おまけ:特定の条件下の時だけ発火したい
上記の手法は useEffect の第二引数に何もデータを設定していないため、コンポーネントがレンダリングされた直後にイベント指定が行われ、その後変更が起こることはありません。
その為、実際に利用すると 「画面を表示したら何をしても画面遷移時に確認モーダルが表示される」 といった状態になります。
目的の動作がそれであれば良いですが、実際のアプリの運用などでは「フォームの内容を書き換え、保存せずに画面を遷移する際に警告の意味で表示する」といった 特定の条件下の場合だけモーダルを表示したい という要件の方が多いと思われます。
例として、変更されているかどうかを保持する boolean の state 変数 changed を用意した場合を以下に示します。
const [changed, setChanged] = useState(false)
const router = useRouter()
const pageChangeHandler = () => {
const answer = window.confirm('本当にページ遷移しますか?')
if (!answer) {
throw 'Abort route'
}
}
const beforeUnloadhandler = (event) => {
event.returnValue = '本当にページ遷移しますか?'
}
useEffect(() => {
if (changed) {
router.events.on('routeChangeStart', pageChangeHandler)
window.addEventListener('beforeunload', beforeUnloadhandler)
return () => {
router.events.off('routeChangeStart', pageChangeHandler)
window.removeEventListener('beforeunload', beforeUnloadhandler)
}
}
}, [changed])
useEffect の第二引数に state 変数 changed が指定されたことで、コンポーネントのレンダリングが完了した時点のみならず changed の値が変更される度に第一引数の内容が実行されます。
そのため、上記の例の他に値の変更が行われる個所などで実際に setChanged で true に変更する箇所を用意しておけば、以下の様な挙動となります。
・レンダリング直後は false のためイベントの指定が行われず、結果としてページ遷移時にモーダルの表示は行われない
・値の変更などの所に合わせて true に変更されるとイベントの指定が行われ、ページの遷移時にモーダルが表示されるようになる
・内容を保存する際に true から false に戻す 処理も追加している場合、保存後は再度イベントの指定が解除され、ページ遷移時にモーダルの表示も行われなくなる
終わりに
今回は Next.js でページ遷移時の確認モーダルを出す方法についての備忘録を記載しました。
おまけの方で特定の条件下の時だけ発火する場合について記載しましたが、formik を使う場合などで state 変数の変更につまずいた点もあるため、そちらについては後日時間があれば別の記事で当時の対応を残すことが出来ればと思います。