next/routerのページ遷移中に確認ダイアログを挟みたい
開発環境
- next 13.0.2
本題
ページ遷移をする前に処理を挟みたい事例があったので、模索した内容をまとめてみます。
今回の想定するケースはユーザーが編集をする画面で、かつ、その編集画面内にはナビゲーションなど様々なサイト内リンクがあることを前提にしています。
処理の流れは以下です。
- 編集画面でユーザーが何かしらの編集を行うと「編集中の状態をstateで管理」する
- 状態が編集中の状態でnext/routerのrouter.pushを行うとページ遷移するかどうか確認ダイアログを出す
- 2の結果を受けて、Yes or No でページ遷移する or しない
そして、本記事で扱うのは2~3の処理についてです。
まず、編集ページにはリンクがたくさんある想定なので、共通の処理を実装できれば個別に実装する必要がないので、色々探したところ公式のこちらが利用できそうなので、こちらをベースに実装します。
色々なページで利用したいので、下記のように使えるコンポーネントにします。
<LinkGuard isGuard={true or false}>
<Contents />
</LinkGuard>
isGuardは確認ダイアログを有効にするかどうかの為に入れています。
このLinkGuardコンポーネントの中でnext/routerの処理を中断させる実装をしていきます。
実装
import { useRouter } from 'next/router'
import { PropsWithChildren, FC, useState, useEffect, useContext } from 'react'
import { AlertContext } from '@/contexts/AlertContext' // ※
interface LinkGuardProps extends PropsWithChildren {
isGuard: boolean
}
export const LinkGuard: FC<LinkGuardProps> = ({ isGuard = false, children }) => {
const [path, setPath] = useState('') // ③
const { open } = useContext(AlertContext) // ※
const router = useRouter()
// ①
useEffect(() => {
const handleChangeStart = (url: string) => {
if (isGuard && !path) {
setPath(url)
router.events.emit('routeChangeError')
throw 'aborted'
}
}
router.events.on('routeChangeStart', handleChangeStart)
return () => {
router.events.off('routeChangeStart', handleChangeStart)
}
}, [router, isGuard, path])
// ②
useEffect(() => {
if (!path) return
const handleRouter = async (path: string) => {
if (await open({ message: '編集した内容が保存されていません。\nページを移動しますか?' })) {
await router.push(path)
}
setPath('')
}
handleRouter(path)
}, [path])
return <>{children}</>
}
※本記事では独自で実装したopen関数を利用していますが、内容は割愛しますのでご了承ください。こちらの関数を呼び出すことで、confirmAPIと同じようにモーダルを表示させています。
解説
routerイベントをサブスクライブしています。(①)
そして、router.events.on('routeChangeStart', handleChangeStart)の記述部分でルートが変更され始めたときに発火する関数を定義することができます。
この関数の中で処理を止められそうですが、この中でopen関数のような非同期関数を実行してもページ遷移を止めることができません。
完全に中断させる方法として、エラーをthrowすることで止めることができます。
router.events.emit('routeChangeError')
throw 'aborted'
こちらを利用して、一旦、ページ遷移を完全に中断させます。
その際に、handleChangeStartの関数の引数でページ遷移先のパスを取得できるので、stateで保持します(③)。
そして、改めてページ先のパスをstateで保持した時にopen関数を発火させ、結果に応じて再度router.pushを発火させます。(②)
if (await open({ message: '編集した内容が保存されていません。\nページを移動しますか?' })) {
await router.push(path)
}
さいごに
結果的に以下のような流れで処理することになりました。
- routerイベントをサブスクライブしてページ遷移を中止する
- 再度routerイベントを発火させる
ちなみに、confirmを利用すれば止められます!
useEffect(() => {
const handleChangeStart = (_: string) => {
if (!confirm('ページ遷移しますか?')) {
router.events.emit('routeChangeError')
throw 'aborted'
}
}
router.events.on('routeChangeStart', handleChangeStart)
return () => {
router.events.off('routeChangeStart', handleChangeStart)
}
}, [router])