I am making a simple table in react where I have pagination (separate component) and sorting.
The table should sort update when table header is clicked. It changes state of data. The data is sorted but it doesn't update. The table is rerendered when I change page (pagination component) and it does have sorted data.
The problem is with
this.state.pageOfItems.map((element, index) =>
if I change it to
this.state.data.map((element, index) =>
then sorting works, but pagination doesn't
Please help me get the table data updated immidiately when table header is clicked.
Table component:
import React, { Component } from 'react';
import classes from './Table.css';
import data from 'data.json';
import Pagination from '../Pagination/Pagination';
class Table extends Component {
constructor(props){
super(props);
this.state = {
data: props.data,
headings: Object.keys(props.data[0]),
sortOrder: props.sortOrder || "original",
sortKey: props.sortKey || null,
pageOfItems: []
};
this.sortHandler = this.sortHandler.bind(this);
this.onChangePage = this.onChangePage.bind(this);
}
componentWillMount() {
//TODO: only apply this to the root element of the table component
document.addEventListener('dragover', (event) => {
event.preventDefault();
});
}
sortHandler(e){
const sortKey = e.target.dataset.sortcolumn;
const currentSortKey = this.state.sortKey;
const currentSortOrder = (sortKey === currentSortKey) ? this.state.sortOrder : "original";
this.setState({sortKey: e.target.dataset.sortcolumn});
console.log(currentSortOrder)
switch(currentSortOrder){
case "original":
this.setState({sortOrder: "ascending"});
this.state.data.sort((a, b)=>{
if (a[sortKey] < b[sortKey]) {
return -1;
}
if (a[sortKey] > b[sortKey]) {
return 1;
}
return 0;
});
break;
case "ascending":
this.setState({sortOrder: "descending"});
this.state.data.sort((a, b)=>{
if (a[sortKey] < b[sortKey]) {
return 1;
}
if (a[sortKey] > b[sortKey]) {
return -1;
}
return 0;
});
break;
case "descending":
this.setState({sortOrder: "original"});
this.state.data = this.props.data;
break;
}
this.setState({data: this.state.data});
console.log(this.state.data);
}
onChangePage(pageOfItems) {
// update state with new page of items
this.setState({ pageOfItems: pageOfItems });
}
render() {
return (
<div role="region" aria-labelledby={this.props.id} tabindex="0" style={{overflow: 'auto'}}>
<table cellSpacing="0" cellPadding="0" data-sortorder={this.state.sortOrder}>
{this.props.caption &&
<caption id={this.props.id}>{this.props.caption}</caption>
}
<thead>
<tr>
{this.state.headings.map((element, index) => {
return (
<th
data-sortcolumn={element}
id={'header' + index}
onClick={this.sortHandler}>
{element}
</th>
)
})}
</tr>
</thead>
<tbody>
{this.state.pageOfItems.map((element, index) => {
return (
<tr
id={'row' + index}>
{element && Object.values(element).map((cell)=>{
return <td>
{cell}
</td>
})}
</tr>
)
})}
</tbody>
</table>
<Pagination items={this.state.data} onChangePage={this.onChangePage} />
</div>
);
}
}
Pagination component:
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
items: PropTypes.array.isRequired,
onChangePage: PropTypes.func.isRequired,
initialPage: PropTypes.number
}
const defaultProps = {
initialPage: 1
}
class Pagination extends React.Component {
constructor(props) {
super(props);
this.state = { pager: {} };
}
componentWillMount() {
this.setPage(this.props.initialPage);
}
setPage(page) {
var items = this.props.items;
var pager = this.state.pager;
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified page
pager = this.getPager(items.length, page);
// get new page of items from items array
var pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);
// update state
this.setState({ pager: pager });
// call change page function in parent component
this.props.onChangePage(pageOfItems);
}
getPager(totalItems, currentPage, pageSize) {
// default to first page
currentPage = currentPage || 1;
// default page size is 10
pageSize = pageSize || 5;
// calculate total pages
var totalPages = Math.ceil(totalItems / 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, totalItems - 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 {
totalItems: totalItems,
currentPage: currentPage,
pageSize: pageSize,
totalPages: totalPages,
startPage: startPage,
endPage: endPage,
startIndex: startIndex,
endIndex: endIndex,
pages: pages
};
}
render() {
var pager = this.state.pager;
return (
<ul className="pagination">
<li className={pager.currentPage === 1 ? 'disabled' : ''}>
<a onClick={() => this.setPage(pager.currentPage - 1)}>< back</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>
</ul>
);
}
}
Pagination.propTypes = propTypes;
Pagination.defaultProps
export default Pagination;
EDIT: A part of the pagination has been moved to table component and now it works correctly
I think the problem may come from the fact that you try changing this.state directly in your method sortHandler before passing it to setState.
Try cloning this.state.data first: var clone = this.state.data.slice(0);
Then manipulate (sort) it, and finally assign it to your new state with this.setState({ data: clone })
Also, you may avoid calling multiple times this.setState() in your method, rather once at then end of your function.
I found several problems here.
First: don't assign state by this.state['arg']=. It's a bad practice. Use this.setState() instead.
Second: this.setState() it's asynchronous function, don't forget. Use callbacks. this.setState({ data }, () => anotherFunction())
In your case you can use lodash cloneDeep() method. It return deep copy of object, so you will not change original object. const data = _.cloneDeep(this.state.data).sort().
So. this.setState({ sortKey: e.target.dataset.sortcolumn }, () => putYourSwitchHere()); After that,
this.setState({sortOrder: "ascending"});
this.state.data.sort((a, b)=>{
if (a[sortKey] < b[sortKey]) {
return -1;
}
if (a[sortKey] > b[sortKey]) {
return 1;
}
return 0;
});
break;
Try to use:
const data = _.cloneDeep(this.state.data).sort((a, b)=>{
if (a[sortKey] < b[sortKey]) {
return -1;
}
if (a[sortKey] > b[sortKey]) {
return 1;
}
return 0;
});
this.setState({sortOrder: "ascending", data});
break;
You have already assign this.state.data, this.state.data = this.props.data will make a lot of errors in your last case.
The problem is in sortHandler function.
You are sorting the data using javascript sort() method. It is mutating method and doesn't create new object so React.js can't detect that the data has changed.
You should use non-mutating method.
Your code should be
const newData = this.state.data.concat().sort(...);
this.setState({data: newData});
Related
I'm trying to make the search function work on this simple react app. I can see that it is getting the new data properly when I log it to the console, but it doesn't seem like the child component that builds the "page" to display the data with pagination is ever being called after the initial rendering. 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: []
};
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={item.title} />
)}
</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!
The issue might be
var pager = this.stata.pager;
Replaced it by
var pager = this.getPager(books.length, page);
Hope this is it
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: {} };
}
componentDidUpdate(prevProps, prevState) {
// reset page if books array has changed
console.log('1', this.props.books?.[0], prevProps.books?.[0])
if (this.props.books !== prevProps.books) {
console.log('this.props.books 2', this.props.books);
this.setPage(this.props.initialPage);
}
}
setPage(page) {
var books = this.props.books;
var pager = this.getPager(books.length, page);
console.log('pager', pager)
if (page < 1 || page > pager.totalPages) {
return;
}
// get new pager object for specified 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
console.log('pageofBooks', pageofBooks)
console.log('books', books)
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, page: 1}) ;
})
.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: []
};
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 => {
console.log('response.data.list', response.data.list)
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() {
console.log('this.state.bookList', this.state.bookList)
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={item.title} />
)}
</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'));
Props shouldn't be stored in the state
Notice that constructor is only called once so the state never had a chance to update
class BookCard extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="book-card">
<div className="image__container">
<img src={this.props.imageUrl} />
</div>
<div className="book-card__header">
<h3>{this.props.authorName}</h3>
<h2>{this.props.title.length > 40 ? this.props.title.slice(0, 40) + '...' : this.props.title}</h2>
</div>
</div>
);
}
}
I'm building a pagination component and I'm struggling to execute a for loop so I can dynamically generate the pages. I initially had a function component, but I want to switch it to a class component so I can manage state in it. (I know, I can use hooks, but Im practicing class components at the moment).
I initially added the for loop in the render method but it is executing the loop twice because the component ir rendering twice. Then, I tried componentDidMount() but it doesn't do anything... then used componentWillMount() and it worked. However, I know this could be bad practice.
Any ideas? See below the component with componentDidMount()
import React, { Component } from 'react';
import styles from './Pagination.module.css';
class Pagination extends Component {
state = {
pageNumbers: [],
selected: '',
};
componentDidMount() {
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
this.state.pageNumbers.push(i);
}
}
classActiveForPagineHandler = (number) => {
this.setState({ selected: number });
};
render() {
return (
<div className={styles.PaginationContainer}>
<nav>
<ul className={styles.PageListHolder}>
{this.state.pageNumbers.map((num) => (
<li key={num}>
<a
href="!#"
className={
this.state.selected === num
? styles.Active
: styles.PageActive
}
onClick={() => {
this.props.paginate(num);
// this.props.classActiveForPagineHandler(num);
}}
>
{num}
</a>
</li>
))}
</ul>
</nav>
</div>
);
}
}
export default Pagination;
You better push all the numbers into array and then update pageNumbers state. this.state.pageNumbers.push(i); does not update state directly, you need use setState after your calculation completes.
componentDidMount() {
const { pageNumbers = [] } = this.state
const { totalDogs, dogsPerPage } = this.props
for (let i = 1; i <= Math.ceil(totalDogs / dogsPerPage); i++) {
pageNumbers.push(i);
}
this.setState({ pageNumbers })
}
Demo link here
you should not update state like this :
this.state.pageNumbers.push(i);
do this:
this.setState((s) => {
return {
...s,
pageNumbers: [...s.pageNumbers, i]
}
})
Do not mutate state directly in react component. Use setState for all updates.
componentDidMount() {
const pageNumbers = [];
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
pageNumbers.push(i);
}
this.setState({ pageNumbers });
}
Alternatively, you can simplify the code using Array.from for this case.
componentDidMount() {
this.setState({
pageNumbers: Array.from(
{ length: Math.ceil(this.props.totalDogs / this.props.dogsPerPage) },
(_, i) => i + 1
),
});
}
I'm doing a sorting for a table, it should sort the columns alphabetically, reverse alphabetically and get back to original form for each time the method is called.
Here is my code:
export default class GenericTable extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
otherStudd: '',
currentSort: 'default',
rows: this.props.rows, // I receive the rows data on props
};
}
onSortChange = index => {
const sortMap = {
default: 'up',
up: 'down',
down: 'default',
};
const { currentSort, currentIndex } = this.state;
const nextSort = currentIndex === index ? sortMap[currentSort] : 'default';
const newRows = [...this.state.rows]; // line 40 - here is the error
switch (nextSort) {
case 'up':
newRows.sort((a, b) => (a.cells[index] <= b.cells[index] ? -1 : 1));
break;
case 'down':
newRows.sort((a, b) => (b.cells[index] <= a.cells[index] ? -1 : 1));
break;
}
this.setState({
rows: newRows,
currentSort: nextSort,
currentIndex: index,
});
};
...
}
I think that the code looks correct but I get an es-lint error message:
Line 40:25: Use callback in setState when referencing the previous state react/no-access-state-in-setstate
It should be done a callback function but I don't know how to make it work.
Any ideas?
Ciao, you could try an approach like this:
orderRows = (data, index, nextSort) => {
switch (nextSort) {
case 'up':
data.sort((a, b) => (a.cells[index] <= b.cells[index] ? -1 : 1));
break;
case 'down':
data.sort((a, b) => (b.cells[index] <= a.cells[index] ? -1 : 1));
break;
}
return data;
}
onSortChange = index => {
const sortMap = {
default: 'up',
up: 'down',
down: 'default',
};
const { currentSort, currentIndex } = this.state;
const nextSort = currentIndex === index ? sortMap[currentSort] : 'default';
this.setState((prevState) => ({
rows: this.orderRows(prevState.rows, index, nextSort),
currentSort: nextSort,
currentIndex: index,
}));
};
Assigning props to the state and using them from therer is a React anti-pattern (you can read more about it here).
When the scenario you are trying to achieve requires that, the recommended way is to create a fully controlled component, or in this case, a partially controlled component where, not only rows are used from the props, but any transformation on them is also done through the props (in this case, sorting).
The snippet below demonstrates a simplified version of the issue at hand. We create a Parent component that stores rows in its state, and also provides a method that performs the sort. It then renders a fully controlled Child component that doesn't need to have its own state and receives the rows as well as its sorter function as props instead.
The sortRows method illustrates how to use setState with a callback that offers the benefit of producing more consistant results on the state compared to providing a state object instead.
class Child extends React.PureComponent {
render() {
const { rows, sortRows } = this.props;
return (
<div>
<ul>{ rows.map((row, i) => (<li key={ i }>{ row }</li>)) }</ul>
<button onClick={ () => { sortRows(true) }}>Sort Ascending</button>
<button onClick={ () => { sortRows(false) }}>Sort Descending</button>
</div>
);
}
}
class Parent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rows: ['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter']
};
}
sortRows = (isSortAscending) => {
this.setState((prevState) => ({
rows: prevState.rows.slice().sort((a, b) => {
if (a < b) return -(isSortAscending ? 1 : -1);
if (a > b) return (isSortAscending ? 1 : -1);
return 0;
})
}));
}
render() {
return <Child rows={ this.state.rows } sortRows={ this.sortRows } />;
}
}
ReactDOM.render(<Parent />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You need to use a setState with callback function that does the data processing and set the new state for you.
Here is a converted version for your case:
onSortChange = index => {
const sortMap = {
default: 'up',
up: 'down',
down: 'default',
};
this.setState(({ rows, currentIndex, currentSort }) => {
const nextSort = currentIndex === index ? sortMap[currentSort] : 'default';
const newRows = [...rows];
switch (nextSort) {
case 'up':
newRows.sort((a, b) => (a.cells[index] <= b.cells[index] ? -1 : 1));
break;
case 'down':
newRows.sort((a, b) => (b.cells[index] <= a.cells[index] ? -1 : 1));
break;
default: break;
}
return {
rows: newRows,
currentSort: nextSort,
currentIndex: index,
};
});
}
I created a suggestions search and its built to break up the fetch based on the current page. The state is console.loged correctly, but the render is one page click event behind. This is obviously not the behavior we want. It seems like the state is being updated fine. I have tried to refactor the code difference ways, and even tried this.forceUpdate()
Here is the code
SearchOrderBar.js
import React, { Component } from "react";
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react';
import "./SearchOrderBar.css";
// import { resolve } from "dns";
// import PropTypes from 'prop-types';
import Pagination from '../Search/Pagination';
class SearchOrderBar extends Component {
constructor(props) {
super(props);
this.text = "";
this.state = {
suggestions: [],
addToQuery: false,
Query: [],
pagesNeeded: 0,
page: 1
};
let searchTerm = null;
const {pageLimit = null, keyTimer = null, } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 10;
this.handlePageClick = this.handlePageClick.bind(this);
this.fetchCallBack = this.fetchCallBack.bind(this);
// this.addToQuery = this.addToQuery.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
}
handlePageClick(page){
this.forceUpdate();
this.setState({
page: page
})
this.fetchCallBack();
}
//This fetch should be called in a dynamic switch case
fetchCallBack() {
let y = this.pageLimit;
let x = this.state.page > 1 ? (this.pageLimit*this.state.page) - this.pageLimit : 0;
// Return a promise
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorders/${searchTerm}/${x}/${y}`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
//console.log('here from callback')
this.setState({
suggestions: []
})
return searchTerm;
}).then( data => {
// console.log(this.totalRecords)sd
//console.log(data)
if (searchTerm.length === 0) {
this.setState({
suggestions: [],
rangeCount_URL: `http://localhost:5000/api/searchorderscount/${searchTerm}`
});
} else {
const suggestions = data.filter(function(v){
if(Object.values(v).includes(searchTerm.toLowerCase()) !== -1 || Object.values(v).includes(searchTerm.toUpperCase()) !== -1){
return v
}
})
console.log(suggestions)
this.text = searchTerm;
this.setState({ suggestions: suggestions.sort()});
}
})
})
}
pageCountCallBack(){
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorderscount/${searchTerm}/`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
// console.log('here from Page Count callback')
this.renderSuggestions();
resolve(searchTerm)
})
})
}
keyUpHandler = (e) => {
if(e.target.value.length >= 3){
this.keyTimer = setTimeout(this.countFetch(e), 1500);
} else {
this.setState(() => {
return {
suggestions : [],
pagesNeeded : 0
}
})
clearTimeout(this.keyTimer);
}
}
keyDownHandler = (e) => {
clearTimeout(this.keyTimer);
}
//Any time text is changed in the text field
countFetch = (e) => {
const value = e.target.value;
this.searchTerm = value;
this.pageCountCallBack().then(data => {
const totalRecords = data[0].rows;
this.setState(() => {
return {pagesNeeded : Math.ceil(totalRecords / this.pageLimit)}
})
//console.log("total" + totalRecords);
//console.log("page limit"+this.pageLimit);
//console.log("Needed" + this.state.pagesNeeded );
})
this.fetchCallBack();
}
renderSuggestions() {
//const { suggestions } = this.state;
const tableStyle = {
'tableLayout': 'fixed',
'overflowWrap': 'break-word'
}
return (
<Table style={tableStyle} celled>
{this.state.suggestions.length === 0 ?
(<Table.Body>
<Table.Cell colSpan="7">
<div className="ui fluid warning icon message">
<Icon name="exclamation triangle" size="huge" color="orange"/>
<div className="content">
<Header>No Records Found</Header>
<p>Try Seaching by one of the following:</p>
<ul>
<dt>Name</dt>
<dt>Order Number</dt>
<dt>Address (Shipping or Billing )</dt>
<dt>Phone Number</dt>
<dt>Email</dt>
</ul>
</div>
</div>
</Table.Cell>
</Table.Body>)
: (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Order#</Table.HeaderCell>
<Table.HeaderCell>Billing Address</Table.HeaderCell>
<Table.HeaderCell>Shipping Address</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Phone Number</Table.HeaderCell>
<Table.HeaderCell>Sales Channel</Table.HeaderCell>
<Table.HeaderCell>Order Date</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.state.suggestions.map((item, index) => (
<Table.Row className="hoverRow">
<Table.Cell key={index} onClick={() => this.addToQuery(item)}>
{item.customerPO}
</Table.Cell>
<Table.Cell>
{item.billToAddress}
</Table.Cell>
<Table.Cell>{item.shipToAddress}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.phone}</Table.Cell>
<Table.Cell>{item.customerContact}</Table.Cell>
<Table.Cell>{item.dateCreated}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
)
}
<Pagination key={this.state.pagesNeeded} tableCols="7" pagesNeeded={this.state.pagesNeeded} btnLimit={5} pageClick={this.handlePageClick} currPage={this.state.page} pageLimit={this.pageLimit}/>
</Table>
);
}
handleIconClick(){
console.log('icon clicked ' + this.state.Query )
}
render() {
const {text} = this.state
//console.log(this.state)
return (
<>
<div className="App-Component">
<div className="App-Search">
<Input icon={{ name: 'search', circular: true, link: true, onClick: () => this.handleIconClick() }} placeholder="Search" value={text} type="text" onKeyUp={this.keyUpHandler} onKeyDown={this.keyDownHandler} className="App-Search"/>
{this.renderSuggestions()}
</div>
</div>
</>
);
}
}
export default SearchOrderBar;
Here is the pagination but I don't think this matters as much for the solution. It is relevant for the page button click.
import React, {Component} from 'react';
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react'
/**
* Helper Method for creating a range of Numbers
* Range )( )
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i<=to) {
range.push(i);
i+=step;
}
}
export default class Pagination extends Component {
constructor(props){
super(props)
const { totalRecords = null, pageNeighbours = 0, rangeCount_URL = this.props.rangeCount_URL, pageArray = [] } = props;
this.pageArray = typeof pageArray === 'array' ? pageArray : [];
}
renderPagination = () => {
//console.log("hello from pagination");
let n = this.props.pagesNeeded;
let pArray = [];
let page = this.props.currPage;
//console.log(n)
if (page > 1){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page-1)}>
<Icon name='chevron left' />
</Menu.Item>)
}
for(let i = (page >1 ? page-1: page); pArray.length < (page > this.props.btnLimit ? this.props.btnLimit+1 : this.props.btnLimit); i++){
//console.log(i);
pArray.push(<Menu.Item index={i} className={i == page ? 'active' : ''} onClick={() => this.props.pageClick(i)} as='a'>{i}</Menu.Item>)
}
if (page < n){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page+1)}>
<Icon name='chevron right' />
</Menu.Item>)
}
this.pageArray = pArray;
return pArray;
}
render(){
const pageCount = (() => {
const totalRecords = this.totalRecords;
if(totalRecords > 0){
return (this.totalPages = Math.ceil(this.totalRecords / this.props.pageLimit))
}
})();
//console.log(this.pageArray);
return(
<Table.Footer>
{ this.props.pagesNeeded > 1 &&
<Table.Row>
<Table.HeaderCell colSpan={this.props.tableCols}>
<Menu floated='right' pagination>
{this.renderPagination()}
</Menu>
</Table.HeaderCell>
</Table.Row>
}
</Table.Footer>
)
}
}
setState is batched and invoked asynchronously, meaning when you call to this.setState({page}) then read this.state.page in fetchCallBack you probably get the "old" page and not the new page.
Either pass the page directly to fetchCallBack
this.fetchCallBack(page)
And read the page from it and not directly from the state
Or call it as the second argument of setState which is a callback that react will invoke right after the state has been updated.
this.setState({ page }, this.fetchCallBack);
At the point fetchCallBack is called, this.state.page is not updated yet because setState is called asynchronously, that's why it's using the old value. Try this:
handlePageClick(page) {
this.setState({ page }, this.fetchCallBack);
}
The callback syntax allows you to run the function in the next iteration.
So I have a list of 5k elements. I want to display them in parts, say each part is 30 items. The list of items is in the component's state. Each item is an object taken from the API. It has properties on which I have to make an API call. By parts, to avoid enormous load time. So this is what I've got so far(simplified):
let page=1;
class GitHubLists extends Component {
constructor(props) {
super(props);
this.state = {
repos: [],
contributors: []
}
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
axios.get(org)
.then(res => setState({contributors: res})
}
handleScroll() {
page++;
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
render() {
const contributors = this.state.contributors.slice(0,30*page).map(contributor =>
<li key={contributor.id}>{contributor.login} {contributor.contributions}<a href={contributor.url}>View on GitHub</a></li>
);
return (
<div onScroll={this.handleScroll}>{contributors}</div>
)
}
}
Like I said each item(contributor in this case) has properties which values are links for the API calls. 3 to be exact. On each one of them, I need to make an API call, count the items inside the response and display them.
You can use react-virtualized (6.8k stars), it has been designed for this purpose.
Here is an official example with a list of 1000 elements or here with a Infinite Loader.
I wrote an easier live example here where you can modify code.
For your problem, you need to do your API calls in the rowRenderer and play with the overscanRowCount to prefetch rows. (docs of the List component)
I've made a simple pagination adapted from another GIST that I've already used that makes total sense for your purpose, you just need to implement your code.
class ItemsApp extends React.Component {
constructor() {
super();
this.state = {
items: ['a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','33'],
currentPage: 1,
itemsPerPage: 30
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({
currentPage: Number(event.target.id)
});
}
render() {
const { items, currentPage, itemsPerPage } = this.state;
// Logic for displaying current items
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = items.slice(indexOfFirstItem, indexOfLastItem);
const renderItems = currentItems.map((item, index) => {
return <li key={index}>{item}</li>;
});
// Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(items.length / itemsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderItems}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
ReactDOM.render(
<ItemsApp />,
document.getElementById('app')
);
https://codepen.io/anon/pen/jLZjQZ?editors=0110
Basically, you should insert your fetched array inside the items state, and change the itemsPerPage value according to your needs, I've set 30 occurrences per page.
I hope it helps =)
Ok, there is definitely something wrong about how I wrote my app. It is not waiting for all API calls to finish. It sets the state (and pushes to contributors) multiple times. This is the full code:
let unorderedContributors = [];
let contributors = [];
class GitHubLists extends Component {
constructor(props) {
super(props);
this.state = {
repos: [],
contributors: [],
currentPage: 1,
itemsPerPage: 30,
isLoaded: false
};
this.handleClick = this.handleClick.bind(this)
}
componentWillMount() {
//get github organization
axios.get(GitHubOrganization)
.then(res => {
let numberRepos = res.data.public_repos;
let pages = Math.ceil(numberRepos/100);
for(let page = 1; page <= pages; page++) {
//get all repos of the organization
axios.get(`https://api.github.com/orgs/angular/repos?page=${page}&per_page=100&${API_KEY}`)
.then(res => {
for(let i = 0; i < res.data.length; i++) {
this.setState((prevState) => ({
repos: prevState.repos.concat([res.data[i]])
}));
}
})
.then(() => {
//get all contributors for each repo
this.state.repos.map(repo =>
axios.get(`${repo.contributors_url}?per_page=100&${API_KEY}`)
.then(res => {
if(!res.headers.link) {
unorderedContributors.push(res.data);
}
//if there are more pages, paginate through them
else {
for(let page = 1; page <= 5; page++) { //5 pages because of GitHub restrictions - can be done recursively checking if res.headers.link.includes('rel="next"')
axios.get(`${repo.contributors_url}?page=${page}&per_page=100&${API_KEY}`)
.then(res => unorderedContributors.push(res.data));
}
}
})
//make new sorted array with useful data
.then(() => {contributors =
_.chain(unorderedContributors)
.flattenDeep()
.groupBy('id')
.map((group, id) => ({
id: parseInt(id, 10),
login: _.first(group).login,
contributions: _.sumBy(group, 'contributions'),
followers_url: _.first(group).followers_url,
repos_url: _.first(group).repos_url,
gists_url: _.first(group).gists_url,
avatar: _.first(group).avatar_url,
url: _.first(group).html_url
}))
.orderBy(['contributions'],['desc'])
.filter((item) => !isNaN(item.id))
.value()})
.then(() =>
this.setState({contributors, isLoaded: true})
)
)
})
}
})
}
handleClick(event) {
this.setState({currentPage: Number(event.target.id)})
}
render() {
const { contributors, currentPage, contributorsPerPage } = this.state;
//Logic for displaying current contributors
const indexOfLastContributor = currentPage * contributorsPerPage;
const indexOfFirstContributor = indexOfLastContributor - contributorsPerPage;
const currentContributors = contributors.slice(indexOfFirstContributor, indexOfLastContributor);
const renderContributors = currentContributors.map((contributor, index) => {
return <li key={index}>{contributor}</li>;
});
//Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(contributors.length / contributorsPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderContributors}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
How can I fix it so the state is set once and then I can render contributors from the state (and making API calls with values of the properties: followers_url, repos_url and gists_url)?