How to correctly update state in array using react? - javascript

In a previous question, I was given an answer on how to update an array, which was achieved in the following way:
onClick(obj, index) {
if (data.chosenBets[index]) {
// Remove object.
data.chosenBets.splice(index, 1);
} else {
// Add object.
data.chosenBets.splice(index, 0, obj);
}
}
This does not trigger a re-render in my UI. How do I update the array (in the same way as above) while triggering a re-render?

Just mutating a state won't trigger re-render. You need to call setState() function:
// class component
onClick = () => {
// update state
this.setState(newState);
}
// functional component
...
const [ state, setState ] = useState();
...
setState(newState);
Also, it's quite important to perform immutable state updates since React relies on refs usually (especially, when using memo() or PureComponent). So, it's better to create new instance of array with the same items.
onClick(obj, index) {
let newData;
if (data.chosenBets[index]) {
newData = data.slice();
newData.chosenBets.splice(index, 1);
} else {
newData = [ obj, ...data ];
}
setState(newData);
}
And you always can use some libraties for immutable update like immer, object-path-immutable etc.

Try avoiding impure functions when writing react codes. Here, splice is an impure method. I would recommend using the below code:
onClick(obj, index) {
if (this.state.data.chosenBets[index]) {
// Remove object.
const data = {
...this.state.data,
chosenBets: this.state.data.chosenBets.filter((cBet, i) => i !== index)
};
this.setState({ data });
} else {
// Add object.
const data = {
...this.state.data,
chosenBets: [ ...this.state.data.chosenBets, obj ]
};
this.setState({ data });
}
}

I am assuming you have that array already saved in your state. Then you can do something like this:
onClick = (idx) => {
let arr = [...this.state.arrayToModify];
arr.splice(idx,1);
this.setState({ arrayToModify: arr });
}
Hope this helps!

I needed to make a copy of the array:
let arr = appState.chosenBets
arr.splice(index, 1)
appState.chosenBets = arr
Rather than simply doing
data.chosenBets.splice(index, 1);

Related

updating a single object key value inside array of objects react redux state

I am trying to update an object and its key value inside an array using react redux but as I am new to react and redux, so I am not finding a good way to do this and also the value, is not updating to.
Here is my action
export const addIngredientToMenuItemCartA = (menu_item_id,timestamp,ingrediant,ingrediant_type,selectedMenuItemIngrediantType)
=> async dispatch => {
dispatch({
type: ADD_INGREDIENT_TO_MENU_ITEM_CART,
payload: {
menu_item_id,
timestamp,
ingrediant,
ingrediant_type,
ingrediant_category_type_blue: selectedMenuItemIngrediantType
}
});
};
Here is my reducer
export default function(state=[],action){
case ADD_INGREDIENT_TO_MENU_ITEM_CART:
let menu_item_id = action.payload.menu_item_id;
let ingrediant = action.payload.ingrediant;
let timestamp = action.payload.timestamp;
let items1 = state.slice();
const itemIndexi1 = items1.findIndex(item => item.menu_item_id === menu_item_id);
if(true){
items1[itemIndexi1].ingrediantTotal = ingrediant.price;
}
items1[itemIndexi1].ingrediants.push(ingrediant);
return items1;
default:
return state;
}
I have an array of the cart which has objects inside it and I want to find that specific objects and then update them but if I update them in the reducer then the values are not being changed in the store.
It seems that you are mutating the objects. There is a simple pattern to loop over the list and handle only the relevant item while creating new objects without mutations.
In your case, your code could look something like this:
case ADD_INGREDIENT_TO_MENU_ITEM_CART: {
const { menu_item_id, ingrediant } = action.payload;
const nextState = state.map(item => {
if (item.menu_item_id !== menu_item_id) {
// not our item, return it as is
return item;
}
// this is our relevant item, return a new copy of it with modified fields
return {
...item,
ingrediantTotal: ingrediant.price,
ingrediants: [
...item.ingrediants,
ingrediant
]
}
});
return nextState;
}
Keep in mind, Objects and Arrays are mutable so we can use the spread syntax (...) or .slice etc.

Prevent Do not mutate state directly. Use setState()

I have a function to add item on bag. It's working pretty cool but I got a warning Do not mutate state directly. Use setState();
How can I use this.setState() instead use this.state.bagList[item.id] = {...item};
addToBag = (item) => {
let itemFound = false;
Object.keys(this.state.bagList).map((key) => {
const bagItem = this.state.bagList[key];
if(bagItem.id === item.id) {
itemFound = true;
bagItem.quantity++;
return bagItem;
} else {
return bagItem;
}
});
if(!itemFound) {
this.state.bagList[item.id] = {...item};
}
const newbagList = this.state.bagList;
this.setState({ bagList: newbagList });
localStorage.setItem("productsOnBag", JSON.stringify(newbagList));
this.showBag();
}
I expect prevent this warning and mutate the state correctly.
You can use setState in the following way to update bagList without the mutation.
this.setState((prevState) => ({
bagList: {
...prevState.bagList,
[item.id]: {...item}
}
}))
Hope this will help!
I tried some other approaches, but I solved the warnings just adding my state in a const and attributing the {...item}. Here is the code:
...
if(!itemFound) {
const addItemToBagList = this.state.bagList;
addItemToBagList[item.id] = {...item };
}
The problem is in the order of your code.
You are modifying state directly here:
if(!itemFound) {
this.state.bagList[item.id] = {...item};
}
That's why it's throwing an error.
You should first create newbagList, and after that modify it and this.setState({ bagList: newbagList });

Testing React Components setState overload which takes a function

I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});

How is React updating his state?

I have a question concerning React and how state must be updated.
Let's say we have a class Players containing in its state an array of objects called players. We want to update one player in this array. I would have done it this way:
class Players extends Component {
state = {
players: []
}
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
}
But my coworker just did it this way, and it's also working:
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
React's function setState update the players array without telling explicitly to do it. So, I have two questions:
Is it using a reference from the find function, and using it to update the players arrays ?
Is one of those ways recommended ?
Thank you all for your explanations !
The difference is that second snippet misuses setState to trigger an update because it uses playerObj dummy property. This could be achieved with forceUpdate.
Neither of these ways are correct. Immutable state is promoted in React as a convention. Mutating existing state may result in incorrect behaviour in components that expect a state to be immutable. They mutate existing player object, and new player.update value will be used everywhere where this object is used, even if this is undesirable.
An idiomatic way to do this is to use immutable objects in state:
updatePlayer = id => {
this.setState(({ players }) => ({
players: players.map(player => ({
...player,
updated: player.id === id
}));
});
}
Notice that setState is asynchronous, updater function has to be used to avoid possible race conditions.
Yes, all it's using a reference. All javascript objects are references so whenever you do a find you get a reference to the object, so mutating it will update it.
const players = this.state.players.map(player => {
return { ...player, updated: player.id === id };
});
this.setState({players: players});
As for the recommended way, you should stick with yours where you explicitly update the state variable that you care about.
Both of them are not correct, because you are mutating state.
The best way is a create a deep copy of this array ( just clone ) and after that make some changes with this cloned array
You can also use lodash _.cloneDeep();
For example
class Example extends React.Component {
state = {
players: [
{id: 0, name: 'John'};
]
};
updatePlayer = id => {
const { players } = this.state;
const clonePlayers = players.map(a => ({...a}));
const player = clonePlayers.find(playerId => playerId === id);
player.name = 'Jack';
this.setState(() => ({
players: clonePlayers
}));
}
render() {
return (
<div>some</div>
);
}
<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>
well, basically they are not the same, your coworkers code is working just because he is using an old reference of the object. so, lets take a look:
updatePlayer = id => {
const players = this.state.players.map(player => {
player.updated = player.id === id ? true:false;
return player
});
this.setState({players: players});
}
on your function, you are creating a new array using your old array, which is the correct way of doing this.
updatePlayer = id => {
const playerObj = this.state.players.find(item => {
return item.id === id
})
if (playerObj) {
playerObj.updated = true
this.setState({ playerObj })
}
}
here your friend is editing the reference of the object that he got using find and then saving a playerObj which is nothing more than the reference of a player from the array that you wanted to edit. after this you should notice that the new state will be something like
this.state = {
players: [p1, p2 ,p3, p4], // each p is a player.
//notice that playerObj is now a reference to some of the P on the players array
playerObj: {
...playerstuff,
updated: true
}
}
hope it helps :)

What is the shortest way to modify immutable objects using spread and destructuring operators

I'm looking for a pure function, to modify my immutable state object. The original state given as parameter must stay untouched. This is especially useful when working with frameworks like Redux and makes working with immutable object in javascript much easier. Especially since working with the object spread operator using Babel is already possible.
I did not found anything better than first copy the object, and than assign/delete the property I want like this:
function updateState(state, item) {
newState = {...state};
newState[item.id] = item;
return newState;
}
function deleteProperty(state, id) {
var newState = {...state};
delete newState[id];
return newState;
}
I feel like it could be shorter
Actions on state, where state is considered immutable.
Adding or Updating the value of a property:
// ES6:
function updateState(state, item) {
return Object.assign({}, state, {[item.id]: item});
}
// With Object Spread:
function updateState(state, item) {
return {
...state,
[item.id]: item
};
}
Deleting a property
// ES6:
function deleteProperty(state, id) {
var newState = Object.assign({}, state);
delete newState[id];
return newState;
}
// With Object Spread:
function deleteProperty(state, id) {
let {[id]: deleted, ...newState} = state;
return newState;
}
// Or even shorter as helper function:
function deleteProperty({[id]: deleted, ...newState}, id) {
return newState;
}
// Or inline:
function deleteProperty(state, id) {
return (({[id]: deleted, ...newState}) => newState)(state);
}
An ES6 solution, that has a bit more support is Object.assign:
const updateState = (state, item) => Object.assign({}, state, { [item.id]: item });
In a Map Function
To do this process within a map function (remove an attribute and add a new attribute on each object), given an array of objects -
const myArrayOfObjects = [
{id: 1, keyToDelete: 'nonsense'},
{id: 2, keyToDelete: 'rubbish'}
];
Delete the attribute keyToDelete, and add a new key newKey with the value "someVar".
myArrayOfObjects.map(({ keyToDelete, ...item}) => { ...item, newKey:'someVar'});
Updating the array to
[
{id: 1, newKey:'someVar'},
{id: 2, newKey:'someVar'}
]
See this great post for more information on the deletion method.
Instead of writing boilerplate code (as answered above: (({[id]: deleted, ...state}) => state)(state)) which is hard to read, you could use some library to do the same:
https://github.com/cah4a/immutable-modify
https://github.com/kolodny/immutability-helper
https://github.com/M6Web/immutable-set
https://github.com/bormind/immutable-setter
For example:
import {remove} from 'immutable-modify'
function updateState(state, item) {
return remove(state, item.id)
}
It's also supports any nested updates:
import {set} from 'immutable-modify'
function updateState(state, item) {
return set(state, 'user.products', (products) => ({
...products,
items: products.items.concat(item),
lastUpdate: Date.now()
}))
}
Try:
const { id, ...noId } = state;
And test:
console.log(noId);
Removing item from an array, just use filter ;)
CASE 'REMOVE_ITEM_SUCCESS':
let items = state.items.filter(element => element._id !== action.id);
return {
...state,
items
}

Categories