Share
Embed
Cancel Next.js Page Navigation: Full Throttle
LX Web Frontend Night: Unleash Next.js
https://github.com/LayerXcom/next-navigation-guard
This is English edition.
Japanese edition is here: https://speakerdeck.com/ypresto/hack-to-prevent-page-navigation-in-next-js
ypresto
September 28, 2024
Transcript
40 slides
-
© LayerX Inc. Cancel Next.js Page Navigation: Full Throttle ypresto
@ LX Web Frontend Night: Unleash Next.js (2024/09/18) (NOTE: This slide is English edition. Original talk was Japanese.) -
© LayerX Inc. 2 Self introduce • LayerX Bakuraku Div.
◦ Invoice Team ◦ SWE ◦ Creating a SaaS for accountants • Frontend-specialized & Full-stack • Splatoon, Photo, Going out with my child ypresto (yuya_presto) -
© LayerX Inc. 3 Bakuraku and Next.js We use React
+ Next.js for newer products Considering migration to Next.js Electronic Books Preservation Invoice Reception / Journal Entry Application / Expense SSO Corporate Credit Cards Invoice Issuing Attendance Management -
© LayerX Inc. 4 • By evolving FE development environment
... • More easy, comfortable and thrilled experience • To both our customers and developers Bakuraku and Next.js “Frontend Vision” by our FE Guild Why we use React for new products • Good for our team including BE SWE • Fewer context switch between products • Accelerates platform dev. with single FW Our statement for frontend development Base: Customer Perspective Performance a11y Multi-device -
© LayerX Inc. 5 “Unsaved Changes” dialog and us Did
you implement “Unsaved Changes” dialog? example.com says You have unsaved changes that will be lost. Cancel OK -
© LayerX Inc. 6 “Unsaved Changes” dialog and us UI
patterns to prevent losing unsaved data Controlled-by-User “Confirmation Dialog” Write-and-Forget “Auto Save” “Draft” -
© LayerX Inc. 7 “Unsaved Changes” dialog and us UI
patterns to prevent losing unsaved data Controlled-by-User “Confirmation Dialog” Write-and-Forget “Auto Save” “Draft” Change-sensitive data like financial or application form Editing already published/submit data Revertible data like internal documents Private data like drafts Required in BtoB SaaS! -
© LayerX Inc. 8 “Unsaved Changes” dialog and us Then,
is yours REALLY shows when back/forward is clicked? It is VERY hard to cancel page navigation with Next.js (which utilizes History API). example.com says You have unsaved changes that will be lost. Cancel OK -
© LayerX Inc. 9 “Unsaved Changes” dialog and us Discussions
in Next.js “For me this worked!” ”No, it does not work when XXX” Infinite Loop https://github.com/vercel/next.js/discussions/9662 https://github.com/vercel/next.js/discussions/47020 App Router plz!!1! Pages Router plz!! -
Navigation Guard library, supports both App Router and Pages Router
We made it! Bet Technology! -
© LayerX Inc. 12 Usage Navigation Guard library, supports both
App Router and Pages Router • In app/layout.tsx or pages/_app.tsx, wrap with <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.reject()}>OK</Button> </Dialog> • • * You can also use enabled: () => isChanged() if necessary. -
Next.js and History API * I’ll talk about App Router
in this slide 3 Hacks to Cancel Page Navigation -
© LayerX Inc. 14 Next.js and History API Non-SPA transition
• Reload Button • External site SPA: In-App Links • <Link> • router.push() • router.replace() • router.refresh() Let’s Cancel Page Navigation: Categolizing • Back / Forward Buttons • router.back() • router.forward() SPA: back/forward -
© LayerX Inc. 15 Next.js and History API Non-SPA transition
• Reload Button • External site SPA: In-App Links • <Link> • router.push() • router.replace() • router.refresh() Let’s Cancel Page Navigation: Categolizing • Back / Forward Buttons • router.back() • router.forward() SPA: back/forward beforeunload event From MDN -
© LayerX Inc. 17 Next.js and History API Non-SPA transition
• Reload Button • External site SPA: In-App Links • <Link> • router.push() • router.replace() • router.refresh() Let’s Cancel Page Navigation: Categolizing • Back / Forward Buttons • router.back() • router.forward() SPA: back/forward beforeunload event Next.js & History API -
© LayerX Inc. 18 Next.js and History API • Change
URL and history from JS without refetching by browser • Then replace page content from JS • history.state Holds custom state data for current page • popstate event Published after stack index and URL changed by back/forward button /posts/123 /posts Self-manage Page Navigation and History by the App SPA and History API /posts/124 state state state history.pushState() to add history.replaceState() to overwrite Back Button history.forward() / history.go(x) Forward Button history.back() / history.go(-x) Current Page / state -
© LayerX Inc. 19 Next.js and History API Non-SPA transition
• Reload Button • External site SPA: In-App Links • <Link> • router.push() • router.replace() • router.refresh() Let’s Cancel Page Navigation: Categolizing • Back / Forward Buttons • router.back() • router.forward() SPA: back/forward beforeunload event 1. Updates internal state of Next.js 2. Updates History -
© LayerX Inc. 20 Next.js Page Navigation : In-App Links
App Router History API history.pushState() What happens when <Link> is clicked <Link> : router.push() React Update Router state Render next page -
© LayerX Inc. 21 Next.js Page Navigation : In-App Links
App Router History API history.pushState() What happens when <Link> is clicked <Link> : router.push() React Update Router state Render next page Can we stop at here? (before state change) -
© LayerX Inc. 22 Next.js Page Navigation : In-App Links
No official event hooks in Next.js Read the code of router.push() in App Router https://github.com/vercel/next.js/blob/9a1cd356dbafbfcf23d1b9ec05f772f766d05580/packages/next/src/client/components/app-router.tsx#L390-L394 Stop!! -
© LayerX Inc. 24 Replace the Router via Context.Provider <AppRouterContext.Provider
value={OfficialRouter}> ... <AppRouterContext.Provider value={WrappedRouter}> {app} </AppRouterContext.Provider> ... </AppRouterContext.Provider> Nearest-parent rule of Context.Provider The {app} sees this one Patching without destructive change of original Router instance. It’s better. -
© LayerX Inc. 25 Next.js and History API Non-SPA transition
• Reload Button • External site SPA: In-App Links • <Link> • router.push() • router.replace() • router.refresh() Let’s Cancel Page Navigation: Categolizing • Back / Forward Buttons • router.back() • router.forward() SPA: back/forward beforeunload event 1. Updates internal state of Next.js 2. Updates History 1. Update index of History 2. Updates state of Next.js Reverse order than <Link> -
© LayerX Inc. 26 Next.js Page Navigation : Back/Forward App
Router History API What happens when Back/Forward button is clicked popstate event React Render next page Change the stack index and the URL Update Router state -
© LayerX Inc. 27 Next.js Page Navigation : Back/Forward App
Router History API What happens when Back/Forward button is clicked popstate event React Render next page Change the stack index and the URL Update Router state We could not preventDefault() the history change. URL already changed even we can stop at here. -
© LayerX Inc. 28 Next.js Page Navigation : Back/Forward Change
the stack index and the URL History API Suppress event then restore the history: Back to the Future! popstate event next-navigation-guard window.confirm() history.go(-delta) popstate event We want to suppress the event here Change the stack index and the URL Revert history change before Next.js realizes it -
© LayerX Inc. 29 Next.js Page Navigation : Back/Forward >
Suppress the event You may know capturing listener + stopPropagation() hack. <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. 30 Next.js Page Navigation : Back/Forward >
Suppress the event No, Chrome does not. Firefox and Safari does 🤦 capture is not called first for popstate event in Chrome. In Firefox it works. -
© LayerX Inc. 32 stopImmediatePropagation() addEventListener() earlier than Next.js, then
stopImmediatePropagation() to suppress Suppresses event listeners added later than this window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", e => e.stopPropagation()) window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", () => ...) window.addEventLisnter("popstate", e => e.stopImmediatePropagation()) window.addEventLisnter("popstate", () => ...) ←Listener in Next.js won’t be called -
© LayerX Inc. 33 addEventListener() earlier than Next.js, then stopImmediatePropagation()
to suppress Successfully suppressed popstate event using stopImmeidatePropagation() useLayoutEffect() is faster than useEffect() 〜〜〜 Code of App Router -
© LayerX Inc. 34 Next.js Page Navigation : Back/Forward Change
the stack index and the URL History API Restore stack position popstate event next-navigation-guard Suppress if delta === 0 window.confirm() history.go(-delta) popstate event Restore to original position delta = history.state.index - renderedIndex Change the stack index and the URL Suppressed! -
© LayerX Inc. 35 Next.js Page Navigation : Back/Forward •
Q. History API? ◦ No, there is no index property. ◦ We even cannot know whether user backed or forwarded. • Q. Next.js? ◦ Internal state of Pages Router only. ◦ It looks like Next.js doesn’t want to expose history.state, maybe. • Q. Navigation API have navigation.currentEntry.index! ◦ No, Safari and Firefox does not support it. • Whatever we does not have any history index information!!! How many the user navigated back or forward…??? -
© LayerX Inc. 37 Overwrite history.pushState() to set index in
history.state • Add index information to state at every pushState() • history.go(renderedIndex - state.index) to restore original stack position ◦ If user backed 2, then forward 2 to revert it. Set index to calculate delta of back/forward 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 -
© LayerX Inc. 39 • We Humanity needs Navigation Guard
oO(Especially for BtoB SaaS) • Our principle is “Bet Technology”, so we made it. • • Hooked into React Context, App Router, History API ◦ Hack #1: Replace the Router via Context.Provider ◦ Hack #2: Suppress an event using stopImmediatePropagation() ◦ Hack #3: Overwrite history.pushState() to set index in history.state • • Use the source, document and specification of library, framework and API, Luke! Can solve almost anything with them! • • (npm|yarn|pnpm) install next-navigation-guard • https://github.com/LayerXcom/next-navigation-guard • • NOTE:history.go(-delta) is inspired by Nuxt. Please implement it in Next.js officially! Wrap up -
© LayerX Inc. 40 • Pages Router? ◦ Just return
false in router.beforePopState(() => ...) suppresses router state change. ◦ But stack (URL) does not restore after that. ◦ Restoring stack position is the same as App Router’s one. • window.confirm() is synchronous, but custom dialog is async. How? ◦ stopImmediatePropagation() then dispatchEvent(new PopStateEvent("popstate", { ... })) Appendix