Jyotishmoy
Jyotishmoy

Reputation: 578

Maintaining state and scroll position in react

I'm trying to build a component that shows a list of blog posts. When a user clicks on a post, it renders the component that shows the details of the post. But when the user hits the back button in the browser, the previous component with posts list re-renders and it looses the previous state and scroll position. Is there a way that I Can save the previous state and the scroll position so that when a user hits the back button they are at the same position and the post list is not re-rendered and doesn't loose the scroll position too?

Here's my blog list component code:

import axios from "axios";
import { Link } from "react-router-dom";

class About extends React.Component {
  state = { posts: [] };

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/posts").then(res => {
      this.setState({ posts: res.data.slice(0, 10) });
    });
  }

  render() {
    const { posts } = this.state;

    const postsList = posts.length ? (
      posts.map(post => {
        return (
          <div className="card" key={post.id}>
            <div className="card-body">
              <Link to={"/view/" + post.id}>
                <h5 className="card-title">{post.title}</h5>
              </Link>
              <p className="card-text">{post.body}</p>
            </div>
          </div>
        );
      })
    ) : (
      <div className="text-danger text-center">No Posts yet...</div>
    );

    return <div>{postsList}</div>;
  }
}

export default About;

Here's is my blog details component:

import React from "react";
import { withRouter } from "react-router-dom";
import axios from "axios";

class PostDetail extends React.Component {
  state = { post: null };

  componentDidMount() {
    let id = this.props.match.params.post_id;

    axios.get("https://jsonplaceholder.typicode.com/posts/" + id).then(res => {
      this.setState({ post: res.data });
    });
  }

  render() {
    const post = this.state.post ? (
      <div className="card border-primary">
        <div className="card-header">{this.state.post.title}</div>
        <div className="card-body text-primary">
          <p className="card-text">{this.state.post.body}</p>
        </div>
      </div>
    ) : (
      <div className="text-center text-danger">Loading Post...</div>
    );

    return <div>{post}</div>;
  }
}

export default withRouter(PostDetail);

Upvotes: 31

Views: 54523

Answers (6)

Unmitigated
Unmitigated

Reputation: 89194

With React Router 6.4+, use the ScrollRestoration component. It requires using a data router, such as one created by calling createBrowserRouter (which is recommended for all new React Router web projects).

This component will emulate the browser's scroll restoration on location changes after loaders have completed to ensure the scroll position is restored to the right spot, even across domains.

To use it, simply render it once in the component for which the scroll position should be maintained when navigating back:

import { ScrollRestoration } from 'react-router-dom';
function About() {
    return <>
        {/* list here */}
        <ScrollRestoration/>
    </>;
}

Upvotes: 6

Muhammad Arsalan Toor
Muhammad Arsalan Toor

Reputation: 191

try the bellow code in your app.js

useEffect(() => {
    const setScroll = () => {
        window.onscroll = (e) => {
            console.log("scrole r", window.scrollY);
            window.scrollY > 12 ? localStorage.setItem("scroll_posistion", window.scrollY) : false;
        };
    }
    let pos = localStorage.getItem("scroll_posistion");
    window.scrollTo(0, pos);
    window.addEventListener('scroll', setScroll);
    // Don't forget to clean up the event listener when the component unmounts
    return () => {
        window.removeEventListener('scroll', setScroll);
    };
}, [0]);

Upvotes: 0

here is the answer using hooks(functional component)

import axios from 'axios';
import { Link } from 'react-router-dom';

const Posts = () => {
  const [posts, setPosts] = useState([]);
  const postsList = posts.length;

  useEffect(() => {
    axios.get('https://jsonplaceholder.typicode.com/posts').then((res) => {
      setPosts(res.data.slice(0, 20));
    });
  }, []);

  useEffect(() => {
    if (posts.length) {
      const scrollPosition = sessionStorage.getItem('scrollPosition');
      if (scrollPosition) {
        window.scrollTo(0, parseInt(scrollPosition, 10));
        sessionStorage.removeItem('scrollPosition');
      }
    }
  }, [posts]);

  return (
    <>
      {postsList ? (
        posts.map((post) => {
          return (
            <div className="card" key={post.id}>
              <div className="card-body">
                <Link
                  to={'/view/' + post.id}
                  onClick={() =>
                    sessionStorage.setItem('scrollPosition', window.pageYOffset)
                  }
                >
                  <h5 className="card-title">{post.title}</h5>
                </Link>
                <p className="card-text">{post.body}</p>
              </div>
            </div>
          );
        })
      ) : (
        <div className="text-danger text-center">No Posts yet...</div>
      )}

      <div>{postsList}</div>
    </>
  );
};

export default Posts;

https://stackblitz.com/edit/react-cbl7in?file=Posts.js

Upvotes: 7

Ved Prakash
Ved Prakash

Reputation: 121

Just in case, someone want to do the same on some specific HTML elements.

const scrollYPosition = document.getElementsByClassName('grid-body')[0].scrollTop;
this.setState({
  toBeExpanded
},() => {
  document.getElementsByClassName('grid-body')[0].scrollTo(0, scrollYPosition);
});

Upvotes: 3

user11910739
user11910739

Reputation:

You have to store the scroll position in state on click of post with the use of window.pageYOffset

this.setState({
    scrollPosition: window.pageYOffset
});

And once you click on back button at that time you have to set the window position in the method of componentDidMount.

window.scrollTo(0, this.state.scrollPosition);

By default you can set the value of scrollPosition is 0.

Updated

Here I have used the sessionStorage to maintain the scroll position for demo purpose. You can also use the context API or redux store to manage it.

Here is the working demo for you. https://stackblitz.com/edit/react-fystht

import React from "react";
import axios from "axios";
import { Link } from "react-router-dom";

class Posts extends React.Component {
  state = { posts: [] };

  componentDidMount() {
    axios.get("https://jsonplaceholder.typicode.com/posts").then(res => {
      this.setState({ posts: res.data.slice(0, 20) }, () => {
        this.handleScrollPosition();
      });
    });
  }

  // handle scroll position after content load
  handleScrollPosition = () => {
    const scrollPosition = sessionStorage.getItem("scrollPosition");
    if (scrollPosition) {
      window.scrollTo(0, parseInt(scrollPosition));
      sessionStorage.removeItem("scrollPosition");
    }
  };

  // store position in sessionStorage
  handleClick = e => {
    sessionStorage.setItem("scrollPosition", window.pageYOffset);
  };

  render() {
    const { posts } = this.state;

    const postsList = posts.length ? (
      posts.map(post => {
        return (
          <div className="card" key={post.id}>
            <div className="card-body">
              <Link to={"/view/" + post.id} onClick={this.handleClick}>
                <h5 className="card-title">{post.title}</h5>
              </Link>
              <p className="card-text">{post.body}</p>
            </div>
          </div>
        );
      })
    ) : (
      <div className="text-danger text-center">No Posts yet...</div>
    );

    return <div>{postsList}</div>;
  }
}

export default Posts;

Hope this will help you!

Upvotes: 29

Shiva Kumar N
Shiva Kumar N

Reputation: 381

Just Check Previous URL and call API:

import React, {Component} from 'react';
import axios from "axios";
import { Link } from "react-router-dom";

class About extends React.Component {

  constructor() {
    super();
    this.state = {
     posts: [],
    };
  }

componentDidMount() {
 var url = document.referrer
 if (url && !url.includes("/view/")) {
   axios.get("https://jsonplaceholder.typicode.com/posts").then(res => {
     this.setState({
       posts: res.data.slice(0, 10),
       isAboutPageOpen: true
     });
   });
 }
}

render() {
 const { posts } = this.state;

 const postsList = posts.length ? (
   posts.map(post => {
     return (
       <div className="card" key={post.id}>
         <div className="card-body">
           <Link to={"/view/" + post.id}>
             <h5 className="card-title">{post.title}</h5>
           </Link>
           <p className="card-text">{post.body}</p>
         </div>
       </div>
     );
    })
  ) : (
   <div className="text-danger text-center">No Posts yet...</div>
 );
 return <div>{postsList}</div>;
 }
}

export default About;

Upvotes: 0

Related Questions