SetState on object property in array - arrays

In my state, I want to update an object property on the first item in an array of objects. The object property isn't changing, even though I am taking steps to ensure immutability.
this.state = {
myFruits: [{a: false, b: 2, c: 'apple'}, {'a': false, b: 4, c: 'kiwi'}, ...],
otherCategory: {}
}
/// further down the code in componentDidUpdate
{ myFruits } = this.state;
let copyFruits = [...myFruits];
let newFruit = { ...copyFruits[0], a : true };
// console.log(newFruit): {a: true, b: 2, c: 'apple'}
copyFruits[0] = newFruit;
// console.log(copyFruits[0)] = {a: true, b: 2, c: 'apple'}
this.setState({ myFruits: copyFruits });
// the end result is the same as the original state, where the first item, apple, has a : false instead of expected a : true
This change is being made in componentDidUpdate, and I'm not sure if this has any effect.

I guess the problem is that your componetDidUpdate method is not getting called.
The code you have written here is perfectly fine.
class SolutionExample extends React.Component {
constructor(){
super()
this.state = {
myFruits: [{a: false, b: 2, c: 'apple'}, {'a': true, b: 4, c: 'kiwi'}],
otherCategory: {}
}
}
changeKey =() =>{
let { myFruits } = this.state;
let copyFruits = [...myFruits]
let newFruit = { ...copyFruits[0], a : true };
copyFruits[0] = newFruit;
this.setState({ myFruits: copyFruits });
}
componentDidUpdate()
{
// this method is called whenever the state is changed }
render() {
let { myFruits } = this.state;
return (
<div>
<h1>Solution</h1>
{myFruits.map((item) =>{
if(item.a){
return(<h2>{item.b + '-' + item.c}</h2>)
}
})}
<button onClick={this.changeKey}>Change state</button>
</div>
);
}
}
ReactDOM.render(<SolutionExample />, document.getElementById('example'));
Here is the link : https://codepen.io/anon/pen/pXYdWQ

You can simplify your change to:
const { myFruits } = this.state;
let newFruit = { ...myFruits[0], a : true };
this.setState({ myFruits: [newFruit, ...myFruits.splice(1)] });
Also, a better approach is to map and update the object that you need by an id or any unique identifier.
this.setState({
myFruits: myFruits.map(fruit => {
if(fruit.c === "apple") {
fruit.a = true
return fruit;
}
return fruit;
})
})

Related

How do I solve the problem of copying data in reference type?

I'm making a simple card game. I want to update the data in state, but the data I keep in data is being updated, how can I solve this problem? I have the right to click 2 times in the game and when similar cards are matched, they disappear. I can't refresh when all cards are matched
I can match the same cards, but I cannot restart when I want to restore them, and when I examine them, I cannot update the data in the state. So in short my reset function doesn't work
my AllCards component
import * as React from 'react'
import { Card } from './components/Card'
import { cardState, ICard } from './Types'
import {data} from './data'
interface IState {
cards: ICard[]
}
class AllCards extends React.Component<{}, IState>{
selectedCardIds: number [] = [];
selectedCards: ICard[] = []
state: IState= {
cards: [...data]
}
cardClickHandler = (card: ICard) =>{
const {cards} =this.state
if (this.selectedCardIds.length< 2){
this.selectedCardIds.push(card.id)
this.selectedCards.push(card)
this.setState({
...this.state,
cards: cards.map(c => c.id === card.id ? card: c)
}, this.checkMarch)
}
//console.log({card});
}
checkMarch = () =>{
if (this.selectedCardIds.length === 2 ){
setTimeout(() => {
let newCards: ICard[] = []
const {cards} =this.state
let nextState: cardState = "unmatched"
if(this.selectedCards[0].content === this.selectedCards[1].content){
nextState = "matched"
}
newCards = cards.map(c=>{
if(this.selectedCardIds.includes(c.id)){
c.state = nextState
}
return c
})
this.selectedCardIds= []
this.selectedCards= []
this.setState({
...this.state,
cards: newCards
})
}, 500);
}
}
reset = () =>{
console.log("reset");
this.selectedCardIds = []
this.selectedCards = []
this.setState({
...this.state,
cards: data
})
}
render() {
const cardList = this.state.cards.map(c => (<Card key={c.id} card={c} clickHandler={this.cardClickHandler}/>))
return (
<div className='container p-3 bg-dark-gray'>
<div className="row row-cols-1 row-cols-md-3 g-4" style={{columnCount:2}}>
{cardList}
</div>
<hr />
<button onClick={this.reset} className='btn btn-primary'> retyrn</button>
</div>
)
}
}
export {AllCards}
If the data you're trying to update is one level deep (i.e., doesn't contain any nested objects or arrays), Talha's answer will work. However, if your data is more than one level deep, you run the risk of accidentally mutating the child object. For example,
const test1 = {
a: 1,
b: 2,
c: {
d: 4,
},
};
// Direct assignment
const test2 = test1;
test2.a = 5;
console.log(test1.a, test2.a); // 5, 5
// Assigning via spreading (shallow cloning)
const test3 = { ...test1 };
test3.b = 7;
console.log(test1.b, test3.b); // 2, 7
test3.c.d = 9;
console.log(test1.c.d, test3.c.d); // 9, 9
This answer describes how to deeply clone an object, which will resolve the above issue.
If you want a quick and dirty way to deep-clone an object, you can use JSON.parse and JSON.stringify like so:
const test1 = {
a: 1,
b: 2,
c: {
d: 4,
},
}
// Assignment via stringifying/de-stringifying (deep cloning)
const test2 = JSON.parse(JSON.stringify(test1));
test2.c.d = 9;
console.log(test1.c.d, test2.c.d); // 4, 9
if you want to get rid of the reference just spread it like
data = {..state.data} // if its object,
data = [...state.data] // if its array
therefore if your state.data gets updated, data will not going to get affected by the changes

What is the best way to update object array value in React

My React state:
//...
this.state = {
mylist: [
{
"id": 0,
"trueorfalse": false
},
{
"id": 1,
"trueorfalse": false
}
]
}
//...
I am trying to update the trueorfalse value based on the id
Here is what I did so far but didn't work:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
var TorF = true
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { TorF }) : el))
})
}
I really want to make it dynamic so the trueorfase will be opposite of what it is now:
var idnum = e.target.id.toString().split("_")[1] //getting the id via an element id (0 or 1 in this case)
if (type === 1) {
this.setState({
mylist: this.state.mylist.map(el => (el.id === idnum ? Object.assign({}, el, { /* if already true set to false or vice versa */ }) : el))
})
}
How can I update my code to have the dynamicity shown in the second example (if possible), otherwise the first example would do just fine
Another solution using map:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mylist: [
{
id: 0,
trueorfalse: false
},
{
id: 1,
trueorfalse: true
}
]
};
}
toggleBoolean = () => {
const ID = Number(this.state.selectedID);
this.setState(prevState => ({
mylist: prevState.mylist.map(item => {
if (item.id === ID) {
return { ...item, trueorfalse: !item.trueorfalse };
} else {
return item;
}
})
}));
};
render() {
return (
<div className="App">
<p>{`State values: ${JSON.stringify(this.state.mylist)}`}</p>
<button onClick={this.toggleBoolean}>Change true/false values</button>
<label>Insert ID:</label>
<input
type="number"
onChange={event => this.setState({ selectedID: event.target.value })}
/>
</div>
);
}
}
I think the following code would accomplish your second question.
var idnum = e.target.id.toString().split("_")[1]
let newList = Array.from(this.state.mylist) //create new array so we don't modify state directly
if (type === 1) {
let objToUpdate = newList.find((el) => el.id === idnum) // grab first element with matching id
objToUpdate.trueorfalse = !objToUpdate.trueorfalse
this.setState( { mylist: newList } )
}

Why i cannot update value of specific index in an array in react js via set State?

I have an array like below
[
1:false,
9:false,
15:false,
19:false,
20:true,
21:true
]
on click i have to change the value of specific index in an array.
To update value code is below.
OpenDropDown(num){
var tempToggle;
if ( this.state.isOpen[num] === false) {
tempToggle = true;
} else {
tempToggle = false;
}
const isOpenTemp = {...this.state.isOpen};
isOpenTemp[num] = tempToggle;
this.setState({isOpen:isOpenTemp}, function(){
console.log(this.state.isOpen);
});
}
but when i console an array it still shows old value, i have tried many cases but unable to debug.
This is working solution,
import React, { Component } from "react";
class Stack extends Component {
state = {
arr: [
{ id: "1", value: false },
{ id: "2", value: false },
{ id: "9", value: false },
{ id: "20", value: true },
{ id: "21", value: true }
]
};
OpenDropDown = event => {
let num = event.target.value;
const isOpenTemp = [...this.state.arr];
isOpenTemp.map(item => {
if (item.id === num) item.value = !item.value;
});
console.log(isOpenTemp);
this.setState({ arr: isOpenTemp });
};
render() {
let arr = this.state.arr;
return (
<React.Fragment>
<select onChange={this.OpenDropDown}>
{arr.map(item => (
<option value={item.id}>{item.id}</option>
))}
</select>
</React.Fragment>
);
}
}
export default Stack;
i hope it helps!
The problem is your array has several empty value. And functions like map, forEach will not loop through these items, then the index will not right.
You should format the isOpen before setState. Remove the empty value
const formattedIsOpen = this.state.isOpen.filter(e => e)
this.setState({isOpen: formattedIsOpen})
Or use Spread_syntax if you want to render all the empty item
[...this.state.isOpen].map(e => <div>{Your code here}</div>)

How do I setState of a deeply nested property in React?

I'm trying to increment the tally property of a given habit, in this context I'm targeting tally: 4. On a related note, I'm open to suggestions for better ways of structuring my data.
class App extends Component {
constructor(props){
super(props)
this.state = {
displayedHabit: "Exercise",
currentUser: "Achilles",
userList: [
{
user: "Achilles",
id: 0,
habits: [
{
habit: "Exercise",
tally: 123
},
{
habit: "Eat Vegetables",
tally: 4
},
]
}
]
}
}
Here is the implementation I've tried after searching for solutions. I don't believe it's working because assignments that use .find() that work after mounting are broken after I call the increment function through an event handler - leading me to believe .find() is no longer being called on an array.
increment = () => {
let newCount = this.state.userList[0].habits[1].tally + 1
this.setState({
userList: {
...this.state.userList[0].habits[1],
tally: newCount
}
})
}
In React, it's very important that you don't mutate your data structures. This essentially means that you have to copy the data structure at each level of your nested data. Since this is too much work, I suggest you use a library made for this purpose. I personally use object-path-immutable.
Example:
import {set} from 'object-path-immutable';
increment = ({userIndex, habitIndex}) => this.setState({
userList: set(this.state.userList, [userIndex, 'habits', habitIndex, 'tally'], this.state.userList[userIndex].habits[habitIndex].tally + 1)
});
I would recommend restructuring your data such that you have objects which actually map the data in accessible ways ie:
this.state = {
displayedHabit: "Exercise",
currentUser: "Achilles",
userList: {
"Achilles": { // Could also be id
id: 0,
habits: {
"Exercise": 123,
"EatVegetables": 4
}
}
}
}
This would allow you to do something like
increment = () => {
const {userList} = this.state;
this.setState({
userList: {
...userList,
Achilles: {
...userList.Achilles
habits: {
...userlist.Achilles.habits
'EatVegetables': userlist.Achilles.habits.EatVegetables + 1
}
}
}
})
}
This would be further simplified by using object-path-immutable which would allow you to do something simple like:
increment = () => {
const {userList} = this.state;
this.setState({
userList: immutable.set(userList, 'Achilles.id.habits.Exercise', userList.Achilles.id.habits.Exercise + 1)
})
}
In order to make this more generic though I would recommend doing what something similar to what earthling suggested:
import {set} from 'object-path-immutable';
incrementUserHabit = ({userIndex, habitIndex}) => this.setState({
userList: set(this.state.userList, [userIndex, 'habits', habitIndex, 'tally'], this.state.userList[userIndex].habits[habitIndex].tally + 1)
});
This way your code is more reusable
Something like this?
class App extends React.Component {
constructor() {
super();
this.state = {
userList: {
'12': {
name: 'Achilles',
habits: {
Exercise: {
tally: 123
},
'Eat Vegetables': {
tally: 231
}
}
}
}
}
}
inc() {
const {userList} = this.state;
userList['12'].habits['Exercise'].tally++;
this.setState({userList});
}
render() {
return (
<div>
<h2>{this.state.userList['12'].habits['Exercise'].tally}</h2>
<button onClick={() => this.inc()}>Increment</button>
</div>
)
}
};
Play with it here:
https://codesandbox.io/s/10w0o6vn0l

ReactJS seems combine two state updates as one render, how to see separate rendering effects?

I am trying to come up a react exercise for the flip-match cards game: say 12 pairs of cards hide (face down) randomly in a 4x6 matrix, player click one-by-one to reveal the cards, when 2 cards clicked are match then the pair is found, other wise hide both again., gane over when all pairs are found.
let stored = Array(I * J).fill(null).map((e, i) => (i + 1) % (I * J));
/* and: randomize (I * J / 2) pairs position in stored */
class Board extends React.Component {
constructor() {
super();
this.state = {
cards: Array(I*J).fill(null),
nClicked: 0,
preClicked: null,
clicked: null,
};
}
handleClick(i) {
if (!this.state.cards[i]) {
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[i] = stored[i];
return {
cards: upCards,
nClicked: prevState.nClicked + 1,
preClicked: prevState.clicked,
clicked: i,
};
}, this.resetState);
}
}
resetState() {
const preClicked = this.state.preClicked;
const clicked = this.state.clicked;
const isEven = (this.state.nClicked-1) % 2;
const matched = (stored[preClicked] === stored[clicked]);
if (isEven && preClicked && clicked && matched) {
// this.forceUpdate(); /* no effects */
this.setState((prevState) => {
const upCards = prevState.cards.slice();
upCards[preClicked] = null;
upCards[clicked] = null;
return {
cards: upCards,
nClicked: prevState.nClicked,
preClicked: null,
clicked: null,
};
});
}
}
renderCard(i) {
return <Card key={i.toString()} value={this.state.cards[i]} onClick={() => this.handleClick(i)} />;
}
render() {
const status = 'Cards: '+ I + ' x ' + J +', # of clicked: ' + this.state.nClicked;
const cardArray = Array(I).fill(null).map(x => Array(J).fill(null));
return (
<div>
<div className="status">{status}</div>
{ cardArray.map((element_i, index_i) => (
<div key={'row'+index_i.toString()} className="board-row">
{ element_i.map((element_j, index_j) => this.renderCard(index_i*J+index_j))
}
</div>
))
}
</div>
);
}
}
Essentially, Board constructor initialize the state, and handleClick() calls setState() to update the state so it trigger the render of the clicked card's value; the callback function resetState() is that if the revealed two card did not match, then another setState() to hide both.
The problem is, the 2nd clicked card value did not show before it goes to hide. Is this due to React combine the 2 setState renderings in one, or is it rendering so fast that we can not see the first rendering effects before the card goes hide? How to solve this problem?
You're passing resetState as the callback to setState, so I would expect after the initial click your state will be reset.
You might want to simplify a bit and do something like this:
const CARDS = [
{ index: 0, name: 'Card One', matchId: 'match1' },
{ index: 1, name: 'Card Two', matchId: 'match2' },
{ index: 2, name: 'Card Three', matchId: 'match1', },
{ index: 3, name: 'Card Four', 'matchId': 'match2' },
];
class BoardSim extends React.Component {
constructor(props) {
super(props);
this.state = {
cardsInPlay: CARDS,
selectedCards: [],
checkMatch: false,
updateCards: false
};
...
}
...
componentDidUpdate(prevProps, prevState) {
if (!prevState.checkMatch && this.state.checkMatch) {
this.checkMatch();
}
if (!prevState.updateCards && this.state.updateCards) {
setTimeout(() => {
this.mounted && this.updateCards();
}, 1000);
}
}
handleCardClick(card) {
if (this.state.checkMatch) {
return;
}
if (this.state.selectedCards.length === 1) {
this.setState({ checkMatch: true });
}
this.setState({
selectedCards: this.state.selectedCards.concat([card])
});
}
checkMatch() {
if (this.selectedCardsMatch()) {
...
}
else {
...
}
setTimeout(() => {
this.mounted && this.setState({ updateCards: true });
}, 2000);
}
selectedCardsMatch() {
return this.state.selectedCards[0].matchId ===
this.state.selectedCards[1].matchId;
}
updateCards() {
let cardsInPlay = this.state.cardsInPlay;
let [ card1, card2 ] = this.state.selectedCards;
if (this.selectedCardsMatch()) {
cardsInPlay = cardsInPlay.filter((card) => {
return card.id !== card1.id && card.id !== card2.id;
});
}
this.setState({
selectedCards: [],
cardsInPlay,
updateCards: false,
checkMatch: false
});
}
render() {
return (
<div>
{this.renderCards()}
</div>
);
}
renderCards() {
return this.state.cardsInPlay.map((card) => {
return (
<div key={card.name} onClick={() => this.handleCardClick(card)}>
{card.name}
</div>
);
});
}
...
}
I've created a fiddle for this you can check out here: https://jsfiddle.net/andrewgrewell/69z2wepo/82425/

Resources