React应用状态变更未重渲染:分页1与2页切换异常求助
React分页组件渲染异常修复
问题描述
在实现书籍列表React应用的分页功能时,出现以下异常:控制台显示每次切换页面时状态均已更新,但第1页与第2页之间切换无法触发内容重渲染,而第2页到第3页、第1页到第3页(或反向)的切换可正常渲染。
完整代码
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'));
问题根源
问题出在BookCard组件的实现上:该组件在构造函数中将props的值存入自身state,但构造函数仅在组件首次挂载时执行。当从第1页切换到第2页时,pageofBooks数组长度不变(均为8条),React会复用已有的BookCard组件实例,而这些实例的state并未同步更新新的props,导致渲染内容仍为旧数据。
而切换到第3页时,数组长度通常小于8,React会卸载多余的组件并创建新的组件实例,新实例会用新props初始化state,因此显示正常。
修复方案
方案1:移除组件state,直接使用props渲染(推荐)
将BookCard改为纯展示组件,无需维护自身state,直接使用传入的props进行渲染:
class BookCard extends React.Component { render() { const { authorName, title, imageUrl } = this.props.book; return ( <div className="book-card"> <div className="image__container"> <img src={imageUrl} /> </div> <div className="book-card__header"> <h3>{authorName}</h3> <h2>{title.length > 40 ? title.slice(0, 40) + '...' : title}</h2> </div> </div> ); } }
方案2:同步props到state(若需保留state)
如果需要保留组件state,可通过componentDidUpdate方法在props变化时更新state:
class BookCard extends React.Component { constructor(props) { super(props); this.state = { author: props.book.authorName, title: props.book.title, image: props.book.imageUrl }; } componentDidUpdate(prevProps) { if (this.props.book !== prevProps.book) { this.setState({ author: this.props.book.authorName, title: this.props.book.title, image: this.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> ); } }
补充优化建议
渲染列表时建议使用书籍的唯一标识(如bookId)作为key,而非数组索引,这样React能更准确地识别组件变化,避免不必要的复用问题:
// 在App组件的render方法中修改列表渲染逻辑 {this.state.pageofBooks.map(item => <BookCard book={item} key={item.bookId} /> )}
内容的提问来源于stack exchange,提问作者Emily Grace Acres
相关产品推荐
相关产品推荐

