Skip to content

Commit

Permalink
fix(experimental scroll): use sessionStorage instead of history m…
Browse files Browse the repository at this point in the history
…utation

Fixes vercel#16690
Fixes vercel#17073
Fixes vercel#20486
  • Loading branch information
Timer committed Dec 31, 2020
1 parent b727299 commit 12200b1
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 41 deletions.
4 changes: 2 additions & 2 deletions packages/next/client/index.tsx
Expand Up @@ -45,7 +45,7 @@ declare global {

type RenderRouteInfo = PrivateRouteInfo & {
App: AppComponent
scroll?: boolean
scroll?: { x: number; y: number } | null
}
type RenderErrorProps = Omit<RenderRouteInfo, 'Component' | 'styleSheets'>

Expand Down Expand Up @@ -753,7 +753,7 @@ function doRender(input: RenderRouteInfo): Promise<any> {
}

if (input.scroll) {
window.scrollTo(0, 0)
window.scrollTo(input.scroll.x, input.scroll.y)
}
}

Expand Down
92 changes: 53 additions & 39 deletions packages/next/next-server/lib/router/router.ts
Expand Up @@ -49,7 +49,10 @@ interface NextHistoryState {
options: TransitionOptions
}

type HistoryState = null | { __N: false } | ({ __N: true } & NextHistoryState)
type HistoryState =
| null
| { __N: false }
| ({ __N: true; idx: number } & NextHistoryState)

let detectDomainLocale: typeof import('../i18n/detect-domain-locale').detectDomainLocale

Expand Down Expand Up @@ -332,7 +335,7 @@ export type AppComponent = ComponentType<AppProps>
type Subscription = (
data: PrivateRouteInfo,
App: AppComponent,
resetScroll: boolean
resetScroll: { x: number; y: number } | null
) => Promise<void>

type BeforePopStateCallback = (state: NextHistoryState) => boolean
Expand All @@ -344,7 +347,14 @@ type HistoryMethod = 'replaceState' | 'pushState'
const manualScrollRestoration =
process.env.__NEXT_SCROLL_RESTORATION &&
typeof window !== 'undefined' &&
'scrollRestoration' in window.history
'scrollRestoration' in window.history &&
!!(function () {
try {
let v = '__next'
// eslint-disable-next-line no-sequences
return sessionStorage.setItem(v, v), sessionStorage.removeItem(v), true
} catch (n) {}
})()

const SSG_DATA_NOT_FOUND = Symbol('SSG_DATA_NOT_FOUND')

Expand Down Expand Up @@ -422,6 +432,8 @@ export default class Router implements BaseRouter {
defaultLocale?: string
domainLocales?: DomainLocales

private _idx: number = 0

static events: MittEmitter = mitt()

constructor(
Expand Down Expand Up @@ -532,27 +544,6 @@ export default class Router implements BaseRouter {
if (process.env.__NEXT_SCROLL_RESTORATION) {
if (manualScrollRestoration) {
window.history.scrollRestoration = 'manual'

let scrollDebounceTimeout: undefined | NodeJS.Timeout

const debouncedScrollSave = () => {
if (scrollDebounceTimeout) clearTimeout(scrollDebounceTimeout)

scrollDebounceTimeout = setTimeout(() => {
const { url, as: curAs, options } = history.state
this.changeState(
'replaceState',
url,
curAs,
Object.assign({}, options, {
_N_X: window.scrollX,
_N_Y: window.scrollY,
})
)
}, 10)
}

window.addEventListener('scroll', debouncedScrollSave)
}
}
}
Expand Down Expand Up @@ -584,7 +575,19 @@ export default class Router implements BaseRouter {
return
}

const { url, as, options } = state
let forcedScroll: { x: number; y: number } | undefined
const { url, as, options, idx } = state
if (process.env.__NEXT_SCROLL_RESTORATION) {
if (manualScrollRestoration) {
// As we're navigating to a new historical index, we need to lookup and
// restore the scroll for that location.
if (this._idx !== idx) {
const v = sessionStorage.getItem('__next_scroll_' + idx)
forcedScroll = v ? JSON.parse(v) : { x: 0, y: 0 }
}
}
}
this._idx = idx

const { pathname } = parseRelativeUrl(url)

Expand All @@ -604,10 +607,11 @@ export default class Router implements BaseRouter {
'replaceState',
url,
as,
Object.assign({}, options, {
Object.assign<{}, TransitionOptions, TransitionOptions>({}, options, {
shallow: options.shallow && this._shallow,
locale: options.locale || this.defaultLocale,
})
}),
forcedScroll
)
}

Expand All @@ -629,6 +633,17 @@ export default class Router implements BaseRouter {
* @param options object you can define `shallow` and other options
*/
push(url: Url, as?: Url, options: TransitionOptions = {}) {
if (process.env.__NEXT_SCROLL_RESTORATION) {
if (manualScrollRestoration) {
try {
// Snapshot scroll position right before navigating to a new page:
sessionStorage.setItem(
'__next_scroll_' + this._idx,
JSON.stringify({ x: self.pageXOffset, y: self.pageYOffset })
)
} catch {}
}
}
;({ url, as } = prepareUrlAs(this, url, as))
return this.change('pushState', url, as, options)
}
Expand All @@ -644,11 +659,12 @@ export default class Router implements BaseRouter {
return this.change('replaceState', url, as, options)
}

async change(
private async change(
method: HistoryMethod,
url: string,
as: string,
options: TransitionOptions
options: TransitionOptions,
forcedScroll?: { x: number; y: number }
): Promise<boolean> {
if (!isLocalURL(url)) {
window.location.href = url
Expand Down Expand Up @@ -781,7 +797,7 @@ export default class Router implements BaseRouter {
// TODO: do we need the resolved href when only a hash change?
this.changeState(method, url, as, options)
this.scrollToHash(cleanedAs)
this.notify(this.components[this.route], false)
this.notify(this.components[this.route], null)
Router.events.emit('hashChangeComplete', as, routeProps)
return true
}
Expand Down Expand Up @@ -1001,7 +1017,7 @@ export default class Router implements BaseRouter {
query,
cleanedAs,
routeInfo,
!!options.scroll
forcedScroll || (options.scroll ? { x: 0, y: 0 } : null)
).catch((e) => {
if (e.cancelled) error = error || e
else throw e
Expand All @@ -1012,12 +1028,6 @@ export default class Router implements BaseRouter {
throw error
}

if (process.env.__NEXT_SCROLL_RESTORATION) {
if (manualScrollRestoration && '_N_X' in options) {
window.scrollTo((options as any)._N_X, (options as any)._N_Y)
}
}

if (process.env.__NEXT_I18N_SUPPORT) {
if (this.locale) {
document.documentElement.lang = this.locale
Expand Down Expand Up @@ -1060,6 +1070,7 @@ export default class Router implements BaseRouter {
as,
options,
__N: true,
idx: this._idx = method !== 'pushState' ? this._idx : this._idx + 1,
} as HistoryState,
// Most browsers currently ignores this parameter, although they may use it in the future.
// Passing the empty string here should be safe against future changes to the method.
Expand Down Expand Up @@ -1227,7 +1238,7 @@ export default class Router implements BaseRouter {
query: ParsedUrlQuery,
as: string,
data: PrivateRouteInfo,
resetScroll: boolean
resetScroll: { x: number; y: number } | null
): Promise<void> {
this.isFallback = false

Expand Down Expand Up @@ -1474,7 +1485,10 @@ export default class Router implements BaseRouter {
}
}

notify(data: PrivateRouteInfo, resetScroll: boolean): Promise<void> {
notify(
data: PrivateRouteInfo,
resetScroll: { x: number; y: number } | null
): Promise<void> {
return this.sub(
data,
this.components['/_app'].Component as AppComponent,
Expand Down

0 comments on commit 12200b1

Please sign in to comment.