◦ Invoice Team ◦ SWE ◦ Creating a SaaS for accountants • Frontend-specialized & Full-stack • Splatoon, Photo, Going out with my child ypresto (yuya_presto)
... • 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
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!
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
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!!
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
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)
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!!
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.
• 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>
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
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.
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
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
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!
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…???
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
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
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