Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Next.js のページ遷移を全力で止める
Search
ypresto
September 18, 2024
Technology
6
3.2k
Next.js のページ遷移を全力で止める
LX Frontend Night: Unleash Next.js
https://github.com/LayerXcom/next-navigation-guard
ypresto
September 18, 2024
Tweet
Share
More Decks by ypresto
See All by ypresto
TypeScriptの型とパフォーマンス (TSKaigi 2024)
ypresto
20
6.6k
アクセシビリティとE2Eテスト
ypresto
0
49
VS Codeのプロセスモデルとデバッグ方法 - パフォーマンスと安定性を支えるアーキテクチャ
ypresto
1
370
TypeScriptの型定義をPRする技術
ypresto
1
660
Other Decks in Technology
See All in Technology
ナレッジグラフとLLMの相互利用
koujikozaki
0
420
Fediverse Discovery Providers overview
andypiper
0
170
不動産 x AIことはじめ~データの真価を拓くために
estie
0
110
あなたの知らないiOS開発の世界
recruitengineers
PRO
3
180
Discovering AI Models
picardparis
4
3.9k
DevRelの始め方
moongift
PRO
1
390
App Router を実プロダクトで採用して見えてきた勘所をちょっとだけ紹介
marokanatani
1
930
『GRANBLUE FANTASY Relink』ソフトウェアラスタライザによる実践的なオクルージョンカリング
cygames
0
170
watsonx.ai Dojo 環境準備について
oniak3ibm
PRO
0
300
Creative UIs with Compose: DroidKaigi 2024
chrishorner
1
580
たった1人からはじめる【Agile Community of Practice】~ソース原理とFearless Changeを添えて~
ktc_corporate_it
1
480
言葉は感情の近似値である。その感情と言葉の誤差を最小化しよう ~コミュニケーションにおけるアナログ/デジタル変換の課題に立ち向かう~
nktamago
0
220
Featured
See All Featured
The Power of CSS Pseudo Elements
geoffreycrofte
71
5.3k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
123
18k
Git: the NoSQL Database
bkeepers
PRO
425
64k
Bash Introduction
62gerente
608
210k
How GitHub Uses GitHub to Build GitHub
holman
472
290k
A designer walks into a library…
pauljervisheath
201
24k
BBQ
matthewcrist
83
9.2k
How to Think Like a Performance Engineer
csswizardry
16
960
It's Worth the Effort
3n
182
27k
The Pragmatic Product Professional
lauravandoore
31
6.2k
Statistics for Hackers
jakevdp
794
220k
From Idea to $5000 a Month in 5 Months
shpigford
379
46k
Transcript
© LayerX Inc. Next.js のページ遷移を全⼒で⽌める!! ypresto @ LX Frontend Night:
Unleash Next.js (2024/09/18)
© LayerX Inc. 3 ⾃⼰紹介 • LayerX バクラク事業部 ◦ 請求書受取‧仕訳チーム
◦ ソフトウェアエンジニア ◦ 経理の⽅が利⽤するプロダクトの開発 • フロントエンドやっていき!でも全部やる • 趣味は主に写真とスプラ、⼦とおでかけ ypresto (プレスト)
© LayerX Inc. 4 バクラクとNext.js バクラクでは、新規プロダクトはReact + Next.jsを採⽤ 将来的にNext.jsへの移⾏を検討中
© LayerX Inc. 5 • バクラクではNext.jsを採⽤しています フロントエンドビジョン by ギルド Reactを採⽤する理由の例
• BEエンジニアを含むメンバーとの相性 • プロダクト間コンテキストスイッチの低減 • フレームワーク統⼀による基盤開発の促進 バクラクのフロントエンドが⽬指す姿
© LayerX Inc. 6 「保存していない変更」を実装したい 「保存されません」ダイアログ、実装してますか?
© LayerX Inc. 7 「保存していない変更」を実装したい 変更の保存漏れを防ぐパターン2選 ユーザーがコントロールする 「確認ダイアログ」 勝⼿にいい感じにする 「⾃動保存」「下書き保存」
© LayerX Inc. 8 「保存していない変更」を実装したい 変更の保存漏れを防ぐパターン2選 ユーザーがコントロールする 「確認ダイアログ」 勝⼿にいい感じにする 「⾃動保存」「下書き保存」
申請や財務情報など、変更が慎重であるべきもの 公開や提出が済んでいる情報の編集 社内ドキュメントなど、戻せば許されるもの 下書きなど、⾃分にしか⾒えていない情報 BtoB SaaSでは必須!
© LayerX Inc. 9 「保存していない変更」を実装したい 「戻る」「進む」ボタンでもちゃんと出ますか??? Next.jsのRouterとHistory APIの都合で、「キャンセル」でページ遷移を止めるのが難しい。
© LayerX Inc. 10 「保存していない変更」を実装したい そのころNext.jsでは 「オレオレ実装の提案」 「それではうまくいきませんね」 を無限ループ中 https://github.com/vercel/next.js/discussions/9662
https://github.com/vercel/next.js/discussions/47020 App Routerで ページ遷移を⽌めたい Pages Routerで ページ遷移のイベントがほしい
Next.js 使っていようとも 全⼈類がページ遷移をキャンセルしたい
App Router & Pages Router に対応した「Navigation Guard」ライブラリ ないものは作る! Bet Technology!
© LayerX Inc. 14 ライブラリの使い⽅ App Router & Pages Router
に対応した「Navigation Guard」ライブラリ • app/layout.tsx か pages/_app.tsx で、<NavigationGuardProvider> で囲う • • useNavigationGuard({ enabled: isChanged, confirm: () => window.confirm()) • • const navGuard = useNavigationGuard({ enabled: isChanged }) <Dialog show={navGuard.active}> <Button onClick={() => navGuard.accept()}>OK</Button> <Button onClick={() => navGuard.decline()}>OK</Button> </Dialog> • • enabled: () => isChanged でもOK
Next.js と History API ※この後は、App Routerを前提に話します ページ遷移を⽌める3つのHack
© LayerX Inc. 16 Next.js と History API SPA的でない遷移 •
リロードボタン • 外部サイトへの遷移 SPA: アプリ内リンク等 • <Link> • router.push() • router.replace() • router.refresh() ページを離脱する遷移を⽌めたい:その分類 • 戻る‧進むボタン • router.back() • router.forward() SPA: back/forward
© LayerX Inc. 17 Next.js と History API SPA的でない遷移 •
リロードボタン • 外部サイトへの遷移 SPA: アプリ内リンク等 • <Link> • router.push() • router.replace() • router.refresh() ページを離脱する遷移を⽌めたい:その分類 • 戻る‧進むボタン • router.back() • router.forward() SPA: back/forward beforeunloadイベント 出典:MDN
制作‧著作:LayerX 終
© LayerX Inc. 19 Next.js と History API SPA的でない遷移 •
リロードボタン • 外部サイトへの遷移 SPA: アプリ内リンク等 • <Link> • router.push() • router.replace() • router.refresh() ページを離脱する遷移を⽌めたい:その分類 • 戻る‧進むボタン • router.back() • router.forward() SPA: back/forward beforeunloadイベント Next.js & History API
© LayerX Inc. 20 Next.js と History API • ブラウザにフェッチさせずに、
URLと履歴だけをJSから書き換える • JSで画⾯の中⾝を書き換える • history.stateで現在ページのstate (カスタ ムでセットした情報) が読める • 戻る‧進む操作でstackの位置が変わると URLが変わってから popstateイベントが発⾏される /posts/123 /posts アプリ側でページ遷移と履歴を⾃前で管理 SPA と History API /posts/124 state state state history.pushState() で積む history.replaceState() で上書き 進むボタン history.forward() / history.go(x) 戻るボタン history.back() / history.go(-x) 現在の ページ / state
© LayerX Inc. 21 Next.js と History API SPA的でない遷移 •
リロードボタン • 外部サイトへの遷移 SPA: アプリ内リンク等 • <Link> • router.push() • router.replace() • router.refresh() ページを離脱する遷移を⽌めたい:その分類 • 戻る‧進むボタン • router.back() • router.forward() SPA: back/forward 1. Next.jsの状態を更新 2. Historyを書き換え beforeunloadイベント
© LayerX Inc. 22 Next.jsのページ遷移:アプリ内リンク等 App Router History API history.pushState()
<Link> クリックでのページ遷移 <Link> が router.push() React Routerの状態を更新 新しいページを描画
© LayerX Inc. 23 Next.jsのページ遷移:アプリ内リンク等 App Router History API history.pushState()
<Link> クリックでのページ遷移 <Link> が router.push() React Routerの状態を更新 新しいページを描画 ここで⽌めたい
© LayerX Inc. 24 Next.jsのページ遷移:アプリ内リンク等 Next.jsに割り込み機能はない App Routerのrouter.push() のコード https://github.com/vercel/next.js/blob/9a1cd356dbafbfcf23d1b9ec05f772f766d05580/packages/next/src/client/components/app-router.tsx#L390-L394
ここで⽌めたい
本⽇の Hack #1 Context 経由で Router を差し替える
© LayerX Inc. 26 Context 経由で Router を差し替える <AppRouterContext.Provider value={公式ルーター}>
... <AppRouterContext.Provider value={wrapしたルーター}> {アプリ} </AppRouterContext.Provider> ... </AppRouterContext.Provider> Contextの値は⼀番近い親のものが使われる アプリから使われるのは こっちになる 元のrouterオブジェクトを 破壊的に変更せずに 差し替えられる
© LayerX Inc. 27 Next.jsのページ遷移:back/forward SPA的でない遷移 • リロードボタン • 外部サイトへの遷移
SPA: アプリ内リンク等 • <Link> • router.push() • router.replace() • router.refresh() ページを離脱する遷移を⽌めたい:その分類 • 戻る‧進むボタン • router.back() • router.forward() SPA: back/forward 1. Next.jsの状態を更新 ←★ 2. Historyを書き換え beforeunloadイベント 1. Historyの位置が変わる 2. Next.jsの状態を更新 ←★
© LayerX Inc. 28 Next.jsのページ遷移:back/forward App Router History API ブラウザの戻る‧進むを押した場合のページ遷移
popstateイベント React 新しいページを描画 スタックの indexを変更 URLも変わる Routerの状態を更新
© LayerX Inc. 29 Next.jsのページ遷移:back/forward App Router History API ブラウザの戻る‧進むを押した場合のページ遷移
popstateイベント React 新しいページを描画 スタックの indexを変更 URLも変わる Routerの状態を更新 Historyの書き換えは キャンセルできない ここで⽌めれてもURLは 変わったまま
© LayerX Inc. 30 Next.jsのページ遷移:back/forward スタックの位置を変更 History API 戻る‧進むされたイベントを握りつぶして、Historyを戻したい popstateイベント
next-navigation-guard キャンセルしたいか確認 history.go(差分) popstateイベント イベントを ここで握りつぶしたい スタックの位置を変更 Next.jsが知らぬうちに 書き戻す
© LayerX Inc. 31 Next.jsのページ遷移:back/forward / イベントを握りつぶす のび太「captureしてstopPropagation()でいけるのでは?」 <body> <div>
<button> Capturing Phase Bubbling Phase <body> <div> <button> div.addEventListener("click", e => e.stopPropagation(), { capture: true }) div.addEventListener("click", e => e.stopPropagation()) <body> <div> <button>
© LayerX Inc. 32 Next.jsのページ遷移:back/forward / イベントを握りつぶす Chromeでは動かん、FirefoxやSafariでは動く Chromeはcapture指定しても 先に呼ばれない!?
Firefoxはcaptureが先
stopImmediatePropagation() で イベントを⽌める 本⽇の Hack #2
© LayerX Inc. 34 stopImmediatePropagation() Next.js より先に addEventListener() して stopImmediatePropagation()
で⽌める 同じ要素に後から追加された Event Listenerを呼びださない window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", e => e.stopPropagation()) window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", e => e.stopImmediatePropagation()) window.addEventLisnter("popstate", () => ...) ←Next.jsは呼ばれない
© LayerX Inc. 35 Next.js より先に addEventListener() して stopImmediatePropagation() で⽌める
popstateイベントを stopImmeidatePropagation() で握りつぶすことに成功 useEffect() より速い useLayoutEffect() で先に刺す 〜〜〜 App Router のコード
© LayerX Inc. 37 Next.jsのページ遷移:back/forward スタックの位置を変更 History API 戻る‧進むされた分のスタック位置を元に戻したい popstateイベント
next-navigation-guard 差分が0なら握りつぶす キャンセルしたいか確認 history.go(差分) popstateイベント 反映済みのindexと history.stateで引き算して 書き戻したい スタックの位置を変更 握りつぶせた!
© LayerX Inc. 38 Next.jsのページ遷移:back/forward • Q. History APIだけで計算できますか ◦
現在ページのindex取れません ◦ popstateで戻ったのか進んだのかさえ不明 • Q. Next.jsはつけてくれませんか ◦ Pages Routerでのinternalの情報しかない ◦ Next.jsはhistory.stateをあまり開放したくないように⾒える • Q. Navigation APIのnavigation.currentEntry.indexは? ◦ SafariとFirefoxでまだサポートされていません • とにかくそのままでは取れないんです!! どれくらい戻る‧進むされたか知りたい...
history.pushState() を上書きして stateにindexを⼊れる 本⽇の Hack #3
© LayerX Inc. 40 Next.jsが呼び出す history.pushState() を 上書きして、stateにindexを⼊れる • 仕⽅がないので⾃前でつけた。さっきのpopのindexなぁに?
🐐 • stackを積むたびにstateにindexを保存 • history.go(描画中のindex - stateのindex) で元の位置に戻せる ◦ 2つ戻されたら、2つ進めて、なかったことに 戻された数だけ進めるため、indexを保存 window.history.pushState = function (state, unused, url) { state = { …state, index: ++currentIndex } origPushState.call(this, state, unused, url) } /posts/123 /posts state.index: 1 state.index: 2 / state.index: 0
(npm|yarn|pnpm) install next-navigation-guard https://github.com/LayerXcom/next-navigation-guard できあがり!
© LayerX Inc. 42 • 全⼈類がページ遷移を⽌めたい oO(BtoB SaaSでは特に) • ⾏動指針
“Bet Technology” なので、ないので作った • • ReactのContext、App Router、History API、それぞれの間に割り込むことで解決できた ◦ Hack #1: Context 経由で Router を差し替える ◦ Hack #2: stopImmediatePropagation() でイベントを⽌める ◦ Hack #3: history.pushState() を上書きしてstateにindexを⼊れる • • ライブラリ、フレームワーク、APIの仕様を把握しコードを読めば、 だいたいなんでも解決できる! • • (npm|yarn|pnpm) install next-navigation-guard • https://github.com/LayerXcom/next-navigation-guard • • 余談:history.go(-delta) は、Nuxtから拝借しました。Next.jsでも公式で実装して欲しいです! まとめ
© LayerX Inc. 43 • Pages Routerの場合は? ◦ popstateに割り込まなくても、router.beforePopState(() =>
...) で割り込み可能 ◦ falseを返すとNext.js側の状態変更はしないが、stackの位置を元に戻してくれない ◦ App Router⽤にstackの位置を元に戻す処理を実装していたので、そのままPages Routerに転 ⽤ • window.confirm()は同期的だけど、カスタムダイアログは⾮同期。どうやって? ◦ stopImmediatePropagation() してから dispatchEvent(new PopStateEvent("popstate", { ... })) してます ちなみに