Skip to content

Instantly share code, notes, and snippets.

@GusRuss89
Last active August 27, 2023 12:51
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save GusRuss89/df05ea25310043fc38a5e2ba3cb0c016 to your computer and use it in GitHub Desktop.
Save GusRuss89/df05ea25310043fc38a5e2ba3cb0c016 to your computer and use it in GitHub Desktop.
Nextjs - keep state and scroll position between page transitions. Described in detail here - https://medium.com/@angus.russell89/next-js-keep-page-components-mounted-between-page-transitions-and-maintain-scroll-position-205b34539a26
import React, { useRef, useEffect, memo } from 'react'
import { useRouter } from 'next/router'
const ROUTES_TO_RETAIN = ['/dashboard', '/top', '/recent', 'my-posts']
const App = ({ Component, pageProps }) => {
const router = useRouter()
const retainedComponents = useRef({})
const isRetainableRoute = ROUTES_TO_RETAIN.includes(router.asPath)
// Add Component to retainedComponents if we haven't got it already
if (isRetainableRoute && !retainedComponents.current[router.asPath]) {
const MemoComponent = memo(Component)
retainedComponents.current[router.asPath] = {
component: <MemoComponent {...pageProps} />,
scrollPos: 0
}
}
// Save the scroll position of current page before leaving
const handleRouteChangeStart = url => {
if (isRetainableRoute) {
retainedComponents.current[router.asPath].scrollPos = window.scrollY
}
}
// Save scroll position - requires an up-to-date router.asPath
useEffect(() => {
router.events.on('routeChangeStart', handleRouteChangeStart)
return () => {
router.events.off('routeChangeStart', handleRouteChangeStart)
}
}, [router.asPath])
// Scroll to the saved position when we load a retained component
useEffect(() => {
if (isRetainableRoute) {
window.scrollTo(0, retainedComponents.current[router.asPath].scrollPos)
}
}, [Component, pageProps])
return (
<div>
<div style={{ display: isRetainableRoute ? 'block' : 'none' }}>
{Object.entries(retainedComponents.current).map(([path, c]) => (
<div
key={path}
style={{ display: router.asPath === path ? 'block' : 'none' }}
>
{c.component}
</div>
))}
</div>
{!isRetainableRoute && <Component {...pageProps} />}
</div>
)
}
export default App
@GusRuss89
Copy link
Author

GusRuss89 commented Apr 12, 2020

This custom app.js file maintains the page component state between specified components, including scroll position.

This is the solution currently in use on NightCafe Creator.

There are a couple of issues with this:

  1. Next will still run the page's getInitialProps function before transitioning the route
  2. The scroll position runs after the render, so it can result in a flash of the 'above the fold' before scrolling to the saved position

I think the only way to solve issue 1 is to keep the state in redux (or another global state manager) and check if the state exists before running any async tasks in getInitialProps.

A better way to manage scroll position would be to put each page inside its own absolutely positioned container that can keep its own scroll position. The container div could be positioned off-screen rather than hidden.

This is described in more detail in a blog post on Medium

@nandorojo
Copy link

Any thoughts on how to achieve this without window scrolling? For instance, if I’m using a ScrollView from React Native Web (which is just a div with overflow-y: scroll) and I want to restore its scroll position. I suppose I could try to track it globally somehow, but it’s a bit cumbersome to solve.

@GusRuss89
Copy link
Author

@nandorojo I'm not familiar with ScrollView but surely it has some way to get and set the scroll position? If so it should be exactly the same but with those methods instead of window.scrollY and window.scrollTo().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment