Slide 1

Slide 1 text

© LayerX Inc. Next.js のページ遷移を全⼒で⽌める!! ypresto @ LX Web Frontend Night: Unleash Next.js (2024/09/18)

Slide 2

Slide 2 text

© LayerX Inc. 3 ⾃⼰紹介 ● LayerX バクラク事業部 ○ 請求書受取‧仕訳チーム ○ ソフトウェアエンジニア ○ 経理の⽅が利⽤するプロダクトの開発 ● フロントエンドやっていき!でも全部やる ● 趣味は主に写真とスプラ、⼦とおでかけ ypresto (プレスト)

Slide 3

Slide 3 text

© LayerX Inc. 4 バクラクとNext.js バクラクでは、新規プロダクトはReact + Next.jsを採⽤ 将来的にNext.jsへの移⾏を検討中

Slide 4

Slide 4 text

© LayerX Inc. 5 ● バクラクではNext.jsを採⽤しています フロントエンドビジョン by ギルド Reactを採⽤する理由の例 ● BEエンジニアを含むメンバーとの相性 ● プロダクト間コンテキストスイッチの低減 ● フレームワーク統⼀による基盤開発の促進 バクラクのフロントエンドが⽬指す姿

Slide 5

Slide 5 text

© LayerX Inc. 6 「保存していない変更」を実装したい 「保存されません」ダイアログ、実装してますか?

Slide 6

Slide 6 text

© LayerX Inc. 7 「保存していない変更」を実装したい 変更の保存漏れを防ぐパターン2選 ユーザーがコントロールする 「確認ダイアログ」 勝⼿にいい感じにする 「⾃動保存」「下書き保存」

Slide 7

Slide 7 text

© LayerX Inc. 8 「保存していない変更」を実装したい 変更の保存漏れを防ぐパターン2選 ユーザーがコントロールする 「確認ダイアログ」 勝⼿にいい感じにする 「⾃動保存」「下書き保存」 申請や財務情報など、変更が慎重であるべきもの 公開や提出が済んでいる情報の編集 社内ドキュメントなど、戻せば許されるもの 下書きなど、⾃分にしか⾒えていない情報 BtoB SaaSでは必須!

Slide 8

Slide 8 text

© LayerX Inc. 9 「保存していない変更」を実装したい 「戻る」「進む」ボタンでもちゃんと出ますか??? Next.jsのRouterとHistory APIの都合で、「キャンセル」でページ遷移を止めるのが難しい。

Slide 9

Slide 9 text

© 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で ページ遷移のイベントがほしい

Slide 10

Slide 10 text

Next.js 使っていようとも 全⼈類がページ遷移をキャンセルしたい

Slide 11

Slide 11 text

App Router & Pages Router に対応した「Navigation Guard」ライブラリ ないものは作る! Bet Technology!

Slide 12

Slide 12 text

© LayerX Inc. 14 ライブラリの使い⽅ App Router & Pages Router に対応した「Navigation Guard」ライブラリ ● app/layout.tsx か pages/_app.tsx で、 で囲う ● ● useNavigationGuard({ enabled: isChanged, confirm: () => window.confirm() }) ● ● const navGuard = useNavigationGuard({ enabled: isChanged }) navGuard.accept()}>OK navGuard.reject()}>OK ● ● enabled: () => isChanged() でもOK

Slide 13

Slide 13 text

Next.js と History API ※この後は、App Routerを前提に話します ページ遷移を⽌める3つのHack

Slide 14

Slide 14 text

© LayerX Inc. 16 Next.js と History API SPA的でない遷移 ● リロードボタン ● 外部サイトへの遷移 SPA: アプリ内リンク等 ● ● router.push() ● router.replace() ● router.refresh() ページを離脱する遷移を⽌めたい:その分類 ● 戻る‧進むボタン ● router.back() ● router.forward() SPA: back/forward

Slide 15

Slide 15 text

© LayerX Inc. 17 Next.js と History API SPA的でない遷移 ● リロードボタン ● 外部サイトへの遷移 SPA: アプリ内リンク等 ● ● router.push() ● router.replace() ● router.refresh() ページを離脱する遷移を⽌めたい:その分類 ● 戻る‧進むボタン ● router.back() ● router.forward() SPA: back/forward beforeunloadイベント 出典:MDN

Slide 16

Slide 16 text

制作‧著作:LayerX 終

Slide 17

Slide 17 text

© LayerX Inc. 19 Next.js と History API SPA的でない遷移 ● リロードボタン ● 外部サイトへの遷移 SPA: アプリ内リンク等 ● ● router.push() ● router.replace() ● router.refresh() ページを離脱する遷移を⽌めたい:その分類 ● 戻る‧進むボタン ● router.back() ● router.forward() SPA: back/forward beforeunloadイベント Next.js & History API

Slide 18

Slide 18 text

© 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

Slide 19

Slide 19 text

© LayerX Inc. 21 Next.js と History API SPA的でない遷移 ● リロードボタン ● 外部サイトへの遷移 SPA: アプリ内リンク等 ● ● router.push() ● router.replace() ● router.refresh() ページを離脱する遷移を⽌めたい:その分類 ● 戻る‧進むボタン ● router.back() ● router.forward() SPA: back/forward 1. Next.jsの状態を更新 2. Historyを書き換え beforeunloadイベント

Slide 20

Slide 20 text

© LayerX Inc. 22 Next.jsのページ遷移:アプリ内リンク等 App Router History API history.pushState() クリックでのページ遷移 が router.push() React Routerの状態を更新 新しいページを描画

Slide 21

Slide 21 text

© LayerX Inc. 23 Next.jsのページ遷移:アプリ内リンク等 App Router History API history.pushState() クリックでのページ遷移 が router.push() React Routerの状態を更新 新しいページを描画 ここで⽌めたい

Slide 22

Slide 22 text

© 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 ここで⽌めたい

Slide 23

Slide 23 text

本⽇の Hack #1 Context 経由で Router を差し替える

Slide 24

Slide 24 text

© LayerX Inc. 26 Context 経由で Router を差し替える ... {アプリ} ... Contextの値は⼀番近い親のものが使われる アプリから使われるのは こっちになる 元のrouterオブジェクトを 破壊的に変更せずに 差し替えられる

Slide 25

Slide 25 text

© LayerX Inc. 27 Next.jsのページ遷移:back/forward SPA的でない遷移 ● リロードボタン ● 外部サイトへの遷移 SPA: アプリ内リンク等 ● ● router.push() ● router.replace() ● router.refresh() ページを離脱する遷移を⽌めたい:その分類 ● 戻る‧進むボタン ● router.back() ● router.forward() SPA: back/forward 1. Next.jsの状態を更新 ←★ 2. Historyを書き換え beforeunloadイベント 1. Historyの位置が変わる 2. Next.jsの状態を更新 ←★

Slide 26

Slide 26 text

© LayerX Inc. 28 Next.jsのページ遷移:back/forward App Router History API ブラウザの戻る‧進むを押した場合のページ遷移 popstateイベント React 新しいページを描画 スタックの indexを変更 URLも変わる Routerの状態を更新

Slide 27

Slide 27 text

© LayerX Inc. 29 Next.jsのページ遷移:back/forward App Router History API ブラウザの戻る‧進むを押した場合のページ遷移 popstateイベント React 新しいページを描画 スタックの indexを変更 URLも変わる Routerの状態を更新 Historyの書き換えは キャンセルできない ここで⽌めれてもURLは 変わったまま

Slide 28

Slide 28 text

© LayerX Inc. 30 Next.jsのページ遷移:back/forward スタックの位置を変更 History API 戻る‧進むされたイベントを握りつぶして、Historyを戻したい popstateイベント next-navigation-guard キャンセルしたいか確認 history.go(差分) popstateイベント イベントを ここで握りつぶしたい スタックの位置を変更 Next.jsが知らぬうちに 書き戻す

Slide 29

Slide 29 text

© LayerX Inc. 31 Next.jsのページ遷移:back/forward / イベントを握りつぶす のび太「captureしてstopPropagation()でいけるのでは?」
Capturing Phase Bubbling Phase
div.addEventListener("click", e => e.stopPropagation(), { capture: true }) div.addEventListener("click", e => e.stopPropagation())

Slide 30

Slide 30 text

© LayerX Inc. 32 Next.jsのページ遷移:back/forward / イベントを握りつぶす Chromeでは動かん、FirefoxやSafariでは動く Chromeはcapture指定しても 先に呼ばれない!? Firefoxはcaptureが先

Slide 31

Slide 31 text

stopImmediatePropagation() で イベントを⽌める 本⽇の Hack #2

Slide 32

Slide 32 text

© 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は呼ばれない

Slide 33

Slide 33 text

© LayerX Inc. 35 Next.js より先に addEventListener() して stopImmediatePropagation() で⽌める popstateイベントを stopImmeidatePropagation() で握りつぶすことに成功 useEffect() より速い useLayoutEffect() で先に刺す 〜〜〜 App Router のコード

Slide 34

Slide 34 text

© LayerX Inc. 37 Next.jsのページ遷移:back/forward スタックの位置を変更 History API 戻る‧進むされた分のスタック位置を元に戻したい popstateイベント next-navigation-guard 差分が0なら握りつぶす キャンセルしたいか確認 history.go(差分) popstateイベント 反映済みのindexと history.stateで引き算して 書き戻したい スタックの位置を変更 握りつぶせた!

Slide 35

Slide 35 text

© 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でまだサポートされていません ● とにかくそのままでは取れないんです!! どれくらい戻る‧進むされたか知りたい...

Slide 36

Slide 36 text

history.pushState() を上書きして stateにindexを⼊れる 本⽇の Hack #3

Slide 37

Slide 37 text

© 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

Slide 38

Slide 38 text

(npm|yarn|pnpm) install next-navigation-guard https://github.com/LayerXcom/next-navigation-guard できあがり!

Slide 39

Slide 39 text

© 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でも公式で実装して欲しいです! まとめ

Slide 40

Slide 40 text

© LayerX Inc. 43 ● Pages Routerの場合は? ○ popstateに割り込まなくても、router.beforePopState(() => ...) で割り込み可能 ○ falseを返すとNext.js側の状態変更はしないが、stackの位置を元に戻してくれない ○ App Router⽤にstackの位置を元に戻す処理を実装していたので、そのままPages Routerに転 ⽤ ● window.confirm()は同期的だけど、カスタムダイアログは⾮同期。どうやって? ○ stopImmediatePropagation() してから dispatchEvent(new PopStateEvent("popstate", { ... })) してます ちなみに