Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

React app not re-rendering on state change

I am trying to build pagination into a simple react app that gets and displays a list of items. It seems like it’s working properly when I log the state to the console because it’s logging something different any time I click a new page. What’s strange is anytime I go from page 2 to 3 or 1 to 3 (or vice versa) it will re-render as expected, but anytime I go between page 1 and 2, no dice. You can ignore the search bar for now, which likewise isn’t working (I’m crossing my fingers that it’s the same rendering issue) but one thing at a time. Codepen & code below:

https://codepen.io/eacres/pen/LYmwmmZ

const propTypes = {
    books: React.PropTypes.array.isRequired,
    onChangePage: React.PropTypes.func.isRequired,
    initialPage: React.PropTypes.number    
}

const defaultProps = {
    initialPage: 1
}

class Pagination extends React.Component {
    constructor(props) {
        super(props);
        this.state = { pager: {} };
    }

    componentWillMount() {
        // set page if books array isn't empty
        if (this.props.books && this.props.books.length) {
            this.setPage(this.props.initialPage);
        }
    }

    componentDidUpdate(prevProps, prevState) {
        // reset page if books array has changed
        if (this.props.books !== prevProps.books) {
            this.setPage(this.props.initialPage);
        }
    }

    setPage(page) {
        var books = this.props.books;
        var pager = this.state.pager;
        
        if (page < 1 || page > pager.totalPages) {
            return;
        }

        // get new pager object for specified page
        pager = this.getPager(books.length, page);

        // get new page of books from books array
      
        var pageofBooks = books.slice(pager.startIndex, pager.endIndex + 1);
      
        // update state
        this.setState({ pager: pager });

        // call change page function in parent component
        this.props.onChangePage(pageofBooks);
    }

    getPager(totalbooks, currentPage, pageSize) {
        // default to first page
        currentPage = currentPage || 1;

        // default page size is 8
        pageSize = pageSize || 8;

        // calculate total pages
        var totalPages = Math.ceil(totalbooks / pageSize);

        var startPage, endPage;
        if (totalPages <= 10) {
            // less than 10 total pages so show all
            startPage = 1;
            endPage = totalPages;
        } else {
            // more than 10 total pages so calculate start and end pages
            if (currentPage <= 6) {
                startPage = 1;
                endPage = 10;
            } else if (currentPage + 4 >= totalPages) {
                startPage = totalPages - 9;
                endPage = totalPages;
            } else {
                startPage = currentPage - 5;
                endPage = currentPage + 4;
            }
        }

        // calculate start and end item indexes
        var startIndex = (currentPage - 1) * pageSize;
        var endIndex = Math.min(startIndex + pageSize - 1, totalbooks - 1);

        // create an array of pages to ng-repeat in the pager control
        var pages = [...Array((endPage + 1) - startPage).keys()].map(i => startPage + i);
        
        // return object with all pager properties required by the view
        return {
            totalbooks: totalbooks,
            currentPage: currentPage,
            pageSize: pageSize,
            totalPages: totalPages,
            startPage: startPage,
            endPage: endPage,
            startIndex: startIndex,
            endIndex: endIndex,
            pages: pages
        };
    }

    render() {
        var pager = this.state.pager;

        if (!pager.pages || pager.pages.length <= 1) {
            // don't display pager if there is only 1 page
            return null;
        }

        return (
            <ul className="pagination">
                <li className={pager.currentPage === 1 ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(1)}>First</a>
                </li>
                <li className={pager.currentPage === 1 ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.currentPage - 1)}>Previous</a>
                </li>
                {pager.pages.map((page, index) =>
                    <li key={index} className={pager.currentPage === page ? 'active' : ''}>
                        <a onClick={() => this.setPage(page)}>{page}</a>
                    </li>
                )}
                <li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.currentPage + 1)}>Next</a>
                </li>
                <li className={pager.currentPage === pager.totalPages ? 'disabled' : ''}>
                    <a onClick={() => this.setPage(pager.totalPages)}>Last</a>
                </li>
            </ul>
        );
    }
}

Pagination.propTypes = propTypes;
Pagination.defaultProps = defaultProps;


/* App Component 
-------------------------------------------------*/

class App extends React.Component {
    constructor() {
        super();

        // an example array of books to be paged
        axios.get(`https://goodreads-server-express--dotdash.repl.co/search/name`)
      .then(response => {
        this.setState({bookList: response.data.list}) ;
      })
      .catch(error => {
        // edge case
        // alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
        console.log(error);
      });
      
        this.state = {
            bookList: [],
            pageofBooks: []
        };

        // bind function in constructor instead of render (https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md)
      //console.log(this.state.pageofBooks)
      this.onChangePage = this.onChangePage.bind(this);
      this.handleChange = this.handleChange.bind(this);
      this.handleSubmit = this.handleSubmit.bind(this);
    }

    onChangePage(pageofBooks) {
        // update state with new page of books
      
        this.setState({ pageofBooks: pageofBooks }, () => {
          console.log(this.state.pageofBooks)
        });
    }
  
    handleChange (e) {
      e.preventDefault();
      this.setState({searchString: e.target.value})
    }

    handleSubmit (e) {
      e.preventDefault();
      this.setState({ bookList : [] });

      // edge case
      if (!this.state.searchString) {
        alert('Oops! Please enter your search in the box below.')
      } else {
        axios.get(`https://goodreads-server-express--dotdash.repl.co/search/${this.state.searchString}`)
        .then(response => {
          this.setState({ bookList: response.data.list });
        })
        .catch(error => {
          // edge case
          alert("Yikes! Looks like we don't have anything for that search. Please edit your search and try again.");
          console.log(error);
        });
      } 
    }

    render() {
        return (
            <div>
                <div className="container">
                    <div className="text-center">
                      <form className="search-bar">
                        <label htmlFor="search">Find me a book</label>
                        <input id="search" onChange={this.handleChange} />
                        <button onClick={this.handleSubmit}>Search</button>
                      </form>
                      <div className="search-results">
                        {this.state.pageofBooks.map( (item, i) =>
                            <BookCard book={item} key={i} />
                        )}
                      </div>
                        <Pagination books={this.state.bookList} onChangePage={this.onChangePage} />
                    </div>
                </div>
                <hr />
            </div>
        );
    }
}

class BookCard extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      author: props.book.authorName,
      title: props.book.title,
      image: props.book.imageUrl
    }
  }

  render() {
    return (
      <div className="book-card">
        <div className="image__container">
          <img src={this.state.image} />
        </div>
        <div className="book-card__header">
          <h3>{this.state.author}</h3>
          <h2>{this.state.title.length > 40 ? this.state.title.slice(0, 40) + '...' : this.state.title}</h2>
        </div>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById('app'));

Thanks in advance!

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>Solution :

Don’t use index as a key

Changing

<BookCard book={item} key={i} />

to

<BookCard book={item} key={item.title} />

fixes the issue

But you should use an id for that

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading