Use callback in setState when referencing the previous state in React - javascript

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,
};
});
}

Related

Adding and deleting array of classes error

Below is my attempt to create an array of classes. The functionality of app is next: one can add or delete extra Input box and increase or decrease its value. As a result the app displays the sum of the all present tags. The issue comes with Delete function, when deleting any of components from created list it does correct math in array but rerenders the elements incorrectly. It always deletes the last component on the list even when you try to remove any others. Any hint why it's happening? Thanks
class Trade1 extends React.Component {
state = {
vl: this.props.value
}
change = (v) => {
let newValue
if (v) {
newValue = this.state.vl + 1
} else {
newValue = this.state.vl - 1
}
this.setState({vl: newValue})
this.props.onChange(newValue, this.props.index)
}
render() {
const {value, index} = this.props
return (
<div>
<button onClick={() => this.change(false)}>Down</button>
<input class="v_price" value={`${this.state.vl}`}/>
<button onClick={() => this.change(true)}>Up</button>
<button onClick={() => this.props.delete(this.props.index)}>Delete</button>
</div>
)
}
}
class Parent extends React.Component {
constructor(props){
super(props);
this.state = {
arr: [0,0,0]
}
}
onChange = (v, i) => {
let newArr = this.state.arr
newArr[i] = v
this.setState(newArr)
}
plus = () => {
let a = this.state.arr
a.push(0)
this.setState({arr: a})
}
minus = i => {
let a = this.state.arr
a.splice(i, 1)
console.log(a)
this.setState({arr: a})
}
render() {
return (
<div>
{this.state.arr.map((v, i) =>
{
return <Trade1 value={v} index={i} onChange={this.onChange} delete={this.minus}/>
}
)}
<div>{
this.state.arr.reduce((a, b) => a+b, 0 )
}</div>
<div><button onClick={this.plus}>Plus</button></div>
</div>
)
}
}
ReactDOM.render(<Parent />, document.getElementById('root'));
You are mutating the array, you should use filter and remove the element at index which you pass as an argument
minus = i => {
this.setState({
arr: this.state.arr.filter((x, j) => j !== i)
})
}
Issue
You've some state mutations. Try to use functional state updates and always return new state objects.
onChange = (v, i) => {
this.setState(prevState => ({
arr: prevState.arr.map((el, index) => index === i ? v : el)
}));
}
plus = () => {
this.setState(prevState => ({
arr: [...prevState.arr, 0],
}));
}
minus = i => {
this.setState(prevState => ({
arr: prevState.arr.filter((_, index) => index !== i),
}));
}

how to add multiple objects in reactjs?

I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues

Component only updates after two clicks React

I am building a React app that - among other things - generates a random number when a button is clicked and then filters an array of JSON objects to only the one at the index of that random number (i.e. JSON[random]). Normally the app is supposed to re-render after the array of JSON objects is filtered, but for some reason, on the first time the button is clicked and a random is picked, it requires two clicks to update. From then on it updates as expected, with a new random rendering each time the button is clicked.
I'm not sure if the problem is coming from App.js or somewhere lower down. On the first click, it generates a new random and supposedly saves this to state, but fails to re-render right away. On subsequent clicks, things seem to update based on the previously-generated random, while a new random is put in the queue. I would prefer the this all happens in one go: click, generate random, save to state, update to reflect the new random à la JSON[random].
This might have something to do with the way I have implemented lifecycle methods, as I'm admittedly not sure of all the nuances of each and have just tried to use whichever ones seemed to do what I wanted. If you have any suggestions there, please let me know...
Thanks!
Here are the relevant files:
App.js - where the random is generated and stored when a new click is registered in Header.state.randomClicks
class App extends Component {
constructor(props){
super(props)
this.state = {headerLink: "", searchValue: "", random: 0, randomClicks: 0}
this.generateRandom = this.generateRandom.bind(this);
}
getLinkFromHeader = (link) => {
if (this.state.headerLink !== link) {
this.setState({
headerLink: link,
})
}
}
getSearchValueFromHeader = (string) => {
this.setState({
searchValue: string,
});
}
getRandomMax = (max) => {
this.setState({
randomMax: max,
})
}
getRandomClicks = (value) => {
this.setState({
randomClicks: value,
})
}
generateRandom(number) {
let random = Math.floor(Math.random() * number) + 1;
console.log("generateRandom = ", random)
return random
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.randomClicks !== nextState.randomClicks;
}
componentWillUpdate() {}
componentDidUpdate(prevState) {
let randomClicks = this.state.randomClicks;
console.log("this.state.randomClicks: ", this.state.randomClicks)
// console.log("prevState: ", prevState)
// console.log("prevState.randomClicks = ", prevState.randomClicks)
// ^^ is this a bug ? ^^
let random = this.generateRandom(this.state.randomMax);
if (this.state.random !== random) {
this.setState({random: random})
}
}
render() {
return (
<div className="App background">
<div className="content">
<Header getLinkFromHeader={this.getLinkFromHeader} getSearchValueFromHeader={this.getSearchValueFromHeader} randomClick={this.randomClick} getRandomClicks={this.getRandomClicks}/>
<TilesContainer link={this.state.headerLink} searchValue={this.state.searchValue} getRandomMax={this.getRandomMax} random={this.state.random} randomClicks={this.state.randomClicks}/>
</div>
</div>
);
}
}
export default App
Header.js* - where the randomClick count is incremented each time RandomButton is clicked
class Header extends Component {
constructor(props){
super(props);
this.state = { selectorLink: "", searchValue: "", randomClicks: 0 }
this.randomClick = this.randomClick.bind(this);
}
getLinkFromSelector = (link) => {
this.setState({
selectorLink: link,
})
}
getSearchValue = (string) => {
this.setState({
searchValue: string,
})
}
shouldComponentUpdate(nextProps, nextState) {
console.log("this.state !== nextState: ", this.state !== nextState)
return this.state !== nextState;
}
componentDidUpdate(previousState){
if(this.state.selectorLink !== previousState.selectorLink) {
this.props.getLinkFromHeader(this.state.selectorLink);
}
this.props.getSearchValueFromHeader(this.state.searchValue);
this.props.getRandomClicks(this.state.randomClicks);
console.log("Header Did Update")
}
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
render(){
return(
<div id="header" className="header">
<div className="title-div">
<div className="h1-wrapper title-wrapper">
<h1>Pokédex Viewer App</h1>
</div>
</div>
<PokedexSelector getLinkFromSelector={this.getLinkFromSelector}/>
<SearchBar getSearchValue={this.getSearchValue}/>
<button type="button" id="random-button" onClick={this.randomClick}>Random Pokémon</button>
<button type="button" id="show-all-button" onClick={this.showAllClick}>Show All</button>
</div>
)
}
}
export default Header
TilesContainer.js - where the random number from App is sent and the tiles list is filtered/re-rendered
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {
pokemon: [],
filteredPokemon: [],
randomMax: 0,
showDetails: false,
};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
this.getPokemon(this.props.link);
}
getPokemon(pokedexLink) {
let link = "";
(pokedexLink === "")
? link = "https://pokeapi.co/api/v2/pokedex/national/"
: link = this.props.link;
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
randomMax: list.length,
})
this.props.getRandomMax; // send randomMax to App
})
}
filterPokemon(string) {
if (string !== "") {
console.log("string: ", string)
string = string.toString().toLowerCase()
let filteredPokemon = this.state.pokemon.filter(pokemon => {
const name = pokemon.pokemon_species.name;
const nameStr = name.slice(0,string.length);
const number = pokemon.entry_number;
const numberStr = number.toString().slice(0, string.length);
return (this.state.random !== 0) ? number.toString() === string : nameStr === string || numberStr === string;
})
if (this.props.randomClicks !== 0) { // i.e. using a random
this.setState({
filteredPokemon: filteredPokemon,
})
} else {
this.setState({
filteredPokemon: filteredPokemon,
randomMax: filteredPokemon.length,
})
}
} else {
this.setState({
filteredPokemon: [],
randomMax: this.state.pokemon.length,
})
}
}
componentDidUpdate(prevProps, prevState) {
if (this.props.link !== prevProps.link) {
this.getPokemon(this.props.link)
}
if (this.props.searchValue !== prevProps.searchValue) {
this.filterPokemon(this.props.searchValue)
}
if (this.state.randomMax !== prevState.randomMax){
this.props.getRandomMax(this.state.randomMax);
}
if (this.props.random !== prevProps.random) {
console.log("TilesContainer random: ", this.props.random)
this.filterPokemon(this.props.random)
}
}
tiles() {
console.log("tiles() filteredPokemon: ", this.state.filteredPokemon)
console.log("tiles() searchValue: ", this.props.searchValue)
console.log("tiles() random: ", this.props.random)
if (this.state.pokemon.length > 0) {
if (this.state.filteredPokemon.length == 0 && this.props.searchValue === ""){
return (
this.state.pokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
} else if (this.state.filteredPokemon.length > 0){
return (
this.state.filteredPokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
}
}
}
render(){
return (
<div id="tiles-container"
className="tiles-container">
{this.tiles()}
</div>
)
}
}
export default TilesContainer
You should not use current state in setState and should not modify state directly. And you do no actually call this.props.randomClick and it is undefined. Change
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
to
randomClick(){
if (typeof(this.props.randomClick) === 'function') this.props.randomClick();
this.setState(olState => ({
randomClicks: olState.randomClicks + 1,
}));
}
Also check your shouldComponentUpdate methods. They might be buggy or redundant. Looks like you prevent updating App when state.random changes. So every time you click the button you store the new random value but use the previous one. So for the initial render and for the first click you use random: 0.
And I guess that getRandomClicks should be setRandomClicks.

React table with sorting and pagination doesn't update

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});

Component can't catch up with redux state

Help me please solve this issue.
I use redux and react-redux to control state in my app.
But when I try to change styles in my Component depending in the value from redux store, it react with delay. When I add new Item and click the list and expect its color being changed, it does this only after I add another item, so that it always delays.
Here is my reducer
export const items = (state = [], action) => {
switch(action.type) {
case 'ADD_ITEM':
const newItem = {
title: action.title,
id: action.id,
selected: action.selected,
comments: action.comments
};
return [
...state,
newItem
];
case 'REMOVE_ITEM':
return state.filter(({ id }) => id !== action.id);
case 'SELECT_ITEM':
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
default:
return state;
}
};
Here is my component which I want to react on every change of the redux store
import React from 'react';
import { connect } from 'react-redux';
import { removeItem, selectItem } from '../actions/items';
import { Badge, ListGroup, ListGroupItem, Button } from 'reactstrap';
const stylesForActiveItem = {
borderLeft: '4px solid red',
transition: 'all .5s',
marginLeft: '-4px',
borderRadius: '0'
}
class Item extends React.Component {
constructor(props) {
super(props);
}
render() {
const { removeItem, selectItem, id, title, selected } = this.props;
return (
<ListGroup className="Item">
<ListGroupItem
onClick={() => selectItem({ id })}
className={selected ? 'Item__text active-item' :
'Item__text'}
>{title} <Badge className="Item__badge">14</Badge>
</ListGroupItem>
<Button className="Item__btn" onClick={() => removeItem({ id
})}>Delete</Button>
</ListGroup>
);
}
}
const mapDispatchToProps = (dispatch) => ({
removeItem: (id) => dispatch(removeItem(id)),
selectItem: (id) => dispatch(selectItem(id))
})
export default connect(null, mapDispatchToProps)(Item);
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
//I guess you need to do something like this
state.map((item) => {
if (item.id === action.id) {
return {...item, selected:true}
} else {
return {...item, selected:false}
}
});
Since even though map returns new array, internal object should also not get mutated. That is why we spread and create a new item object inside.
There is no need to create arrays again in map with entire state. That will just change your state structure instead of just changing a boolean.

Categories

Resources