10

I fetch a list of posts in index js like this :

const Index = (props) => {
    return (
        <div>
            {props.posts.map((each) => {
                return (
                    <Link scroll={false} as={`/post/${each.id}`} href="/post/[id]" key={each.id}>
                        <a>
                            <h1>{each.title}</h1>
                        </a>
                    </Link>
                );
            })}
        </div>
    );
};

export async function getStaticProps() {
    const url = `https://jsonplaceholder.typicode.com/posts`;
    const res = await axios.get(url);

    return {
        props: { posts: res.data }
    };
}

And when user clicks on any link it goes to post page which is :

function post({ post }) {
    return (
            <h1>{post.id}</h1>
    );
}

export async function getServerSideProps({ query }) {
    const { id } = query;
    const res = await Axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);

    return {
        props: { post: res.data}
    };
}

The problem is when I click back the scroll position resets to top and it fetches all posts . I included scroll={false} in Link but it doesn't work .

How can I prevent scroll resetting when user clicks back from the post page ?

Mehdi Faraji
  • 2,574
  • 8
  • 28
  • 76

3 Answers3

21

Next.js in fact has built-in support for restoring scroll position when back to the previous page. We can simply enable it by editing the next.config.js:

module.exports = {
  experimental: {
    scrollRestoration: true,
  },
}
Devo
  • 1,004
  • 11
  • 10
  • 6
    hmm, this did not work for me. – cevaris Apr 07 '22 at 16:00
  • Hi @cevaris, after editing make sure you restart the server – Huseyin Sahin Apr 09 '22 at 15:59
  • As of this date, this doesn't work in Chrome Mac or android but does work for iOS and Mac safari. – Michael Nov 27 '22 at 20:43
  • 1
    @Michael That's strange. This still works very fine for me on the latest Next.js 13, for every browser – Devo Dec 02 '22 at 08:04
  • Great @Devo! Thanks. This works fine using the back button. I did a `site:nextjs.org scrollRestoration` search on Google and expected some documentation. Unsuccessfully. We'll go productive soon and may not use experimental features. I'm looking for a timeline. Does anyone know where I can see plans for integrating this feature? By the way. – Stefan Dec 10 '22 at 11:09
  • 1
    @Stefan I can't find any documentation about its experimental features as well. Here is the list of the experimental features. Maybe you can check if the feature has been removed/modified here before upgrade your production Next.js app. https://github.com/vercel/next.js/blob/canary/packages/next/server/config-shared.ts – Devo Dec 12 '22 at 13:09
  • Here's the updated URL of the experimental features list https://github.com/vercel/next.js/blob/canary/packages/next/src/server/config-shared.ts – Devo Jan 10 '23 at 03:05
5

scroll={false} doesn't maintain the scroll of the previous page; it doesn't change the scroll at all which means the scroll would be that of the page you are linking from. You can use scroll={false} to override the default behavior of setting the scrollY to 0, so you can implement your own behavior.

Here's how I implemented restoring the scroll position. This is very similar to Max william's answer, but using useRef instead of useState. We are using useRef instead of useState because useRef does not cause a re-render whenever its value is mutated, unlike useState. We are going to be updating the value to the current scroll position every time the scroll is changed by the user, which would mean a ton of useless re-renders if we were to use useState.

First, define a UserContext component to easily pass the scroll data from the _app.js component to wherever you need:

import { createContext } from 'react';

const UserContext = createContext();

export default UserContext;

Then, in your _app.js component, wrap your pages with the UserContext and create a useRef attribute to store the scroll position.

import { useRef } from 'react';
import UserContext from '../components/context'

function MyApp({ Component, pageProps }) {

  const scrollRef = useRef({
      scrollPos: 0
  });
    
  return (
      <Layout>
          <UserContext.Provider value={{ scrollRef: scrollRef }}>
              <Component {...pageProps} />
          </UserContext.Provider>
      </Layout>
  )
}

export default MyApp

Then inside whichever page component you are wanting to restore the scroll position (that is, the page that you want to return to and see the same scroll position as when you left), you can put this code to set the scroll position of the page and bind a scroll event to a function to update the stored scroll position.

import UserContext from '../components/context'
import { useContext } from 'react';

export default function YourPageComponent() {

  const { scrollRef } = useContext(UserContext);

  React.useEffect(() => {
    
    //called when the component has been mounted, sets the scroll to the currently stored scroll position
    window.scrollTo(0, scrollRef.current.scrollPos);

    const handleScrollPos = () => {
      //every time the window is scrolled, update the reference. This will not cause a re-render, meaning smooth uninterrupted scrolling.
      scrollRef.current.scrollPos = window.scrollY
    };

    window.addEventListener('scroll', handleScrollPos);

    return () => {
      //remove event listener on unmount
      window.removeEventListener('scroll', handleScrollPos);
    };
  });

  return (
    //your content
  )
}

The last little thing is to use scroll={false} on your Link component that links back to YourPageComponent. This is so next.js doesn't automatically set the scroll to 0, overriding everything we've done.

Credit to Max william's answer to the majority of the structure, my main change is using useRef. I also added some explanations, I hope it's helpful!

Miles
  • 487
  • 3
  • 12
2

I solved the problem with the help of context and window scroll position like this :

import UserContext from '../context/context';

function MyApp({ Component, pageProps }) {
    const [ scrollPos, setScrollPos ] = React.useState(0);
    return (
        <UserContext.Provider value={{ scrollPos: scrollPos, setScrollPos: setScrollPos }}>
            <Component {...pageProps} />
        </UserContext.Provider>
    );
}

export default MyApp;

index js file :

import Link from 'next/link';
import UserContext from '../context/context';
import { useContext } from 'react';
import Axios from 'axios';

export default function Index(props) {
    const { scrollPos, setScrollPos } = useContext(UserContext);

    const handleScrollPos = () => {
        setScrollPos(window.scrollY);
    };

    React.useEffect(() => {
        window.scrollTo(0, scrollPos);
    }, []);

    React.useEffect(() => {
        window.addEventListener('scroll', handleScrollPos);
        return () => {
            window.removeEventListener('scroll', handleScrollPos);
        };
    }, []);

    if (props.err) {
        return <h4>Error bro</h4>;
    }

    return (
        <div>
            {props.res.map((each) => {
                return (
                    <div key={each.id}>
                        <Link scroll={true} as={`/post/${each.id}`} href="/post/[id]">
                            {each.title}
                        </Link>
                    </div>
                );
            })}
        </div>
    );
}

export async function getServerSideProps() {
    let res;
    let err;
    try {
        res = await Axios.get('https://jsonplaceholder.typicode.com/posts');
        err = null;
    } catch (e) {
        err = 'Error bro';
        res = { data: [] };
    }

    return {
        props: {
            res: res.data,
            err: err
        }
    };
}

post js file :

import Axios from 'axios';

function Post(props) {
    if (props.err) {
        return <h4>{props.err}</h4>;
    }
    return <h1>{props.post.title}</h1>;
}

export async function getServerSideProps(ctx) {
    const { query } = ctx;
    let err;
    let res;
    try {
        res = await Axios.get(`https://jsonplaceholder.typicode.com/posts/${query.id}`);
        err = null;
    } catch (e) {
        res = { data: [] };
        err = 'Error getting post';
    }

    return {
        props: {
            post: res.data,
            err: err
        }
    };
}

export default Post;

So when you click back from post js page , the first useEffect in index js will run and you will be scrolled to that position .

Also after that the second useEffect will capture the user's scroll position by listening to the scroll event listener so it will always save the latest scroll y position in context so next time you comeback to index js the first useEffect will run and set scroll position to that value in context .

Mehdi Faraji
  • 2,574
  • 8
  • 28
  • 76
  • 1
    I'm surprised this is working for you - I found that using the useState hook causes the entire page to re-render every time the user scrolls (because you set the state on every scroll event), resulting in visible delays/choppyness when scrolling. A better solution in my opinion is to use the useRef hook, which maintains a value between renders without causing a re-render when set - that way, the value is always up to date with the current scroll but isn't causing needless re-renders. – Miles Oct 11 '20 at 10:06
  • @Miles The useeffect hook will cause the page to re-render all the time if there isn't [ ] after it's function so I used [ ] in my useEffect hook so it only runs once on page load . – Mehdi Faraji Oct 12 '20 at 08:07