Replace array item with another one without mutating state - reactjs

This is how example of my state looks:
const INITIAL_STATE = {
contents: [ {}, {}, {}, etc.. ],
meta: {}
}
I need to be able and somehow replace an item inside contents array knowing its index, I have tried:
return {
...state,
contents: [
...state.contents[action.meta.index],
{
content_type: 7,
content_body: {
album_artwork_url: action.payload.data.album.images[1].url,
preview_url: action.payload.data.preview_url,
title: action.payload.data.name,
subtitle: action.payload.data.artists[0].name,
spotify_link: action.payload.data.external_urls.spotify
}
}
]
}
where action.meta.index is index of array item I want to replace with another contents object, but I believe this just replaces whole array to this one object I'm passing. I also thought of using .splice() but that would just mutate the array?

Note that Array.prototype.map() (docs) does not mutate the original array so it provides another option:
const INITIAL_STATE = {
contents: [ {}, {}, {}, etc.. ],
meta: {}
}
// Assuming this action object design
{
type: MY_ACTION,
data: {
// new content to replace
},
meta: {
index: /* the array index in state */,
}
}
function myReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case MY_ACTION:
return {
...state,
// optional 2nd arg in callback is the array index
contents: state.contents.map((content, index) => {
if (index === action.meta.index) {
return action.data
}
return content
})
}
}
}

Just to build on #sapy's answer which is the correct one. I wanted to show you another example of how to change a property of an object inside an array in Redux without mutating the state.
I had an array of orders in my state. Each order is an object containing many properties and values. I however, only wanted to change the note property. So some thing like this
let orders = [order1_Obj, order2_obj, order3_obj, order4_obj];
where for example order3_obj = {note: '', total: 50.50, items: 4, deliverDate: '07/26/2016'};
So in my Reducer, I had the following code:
return Object.assign({}, state,
{
orders:
state.orders.slice(0, action.index)
.concat([{
...state.orders[action.index],
notes: action.notes
}])
.concat(state.orders.slice(action.index + 1))
})
So essentially, you're doing the following:
1) Slice out the array before order3_obj so [order1_Obj, order2_obj]
2) Concat (i.e add in) the edited order3_obj by using the three dot ... spread operator and the particular property you want to change (i.e note)
3) Concat in the rest of the orders array using .concat and .slice at the end .concat(state.orders.slice(action.index + 1)) which is everything after order3_obj (in this case order4_obj is the only one left).

Splice mutate the array you need to use Slice . And you also need to concat the sliced piece .
return Object.assign({}, state, {
contents:
state.contents.slice(0,action.meta.index)
.concat([{
content_type: 7,
content_body: {
album_artwork_url: action.payload.data.album.images[1].url,
preview_url: action.payload.data.preview_url,
title: action.payload.data.name,
subtitle: action.payload.data.artists[0].name,
spotify_link: action.payload.data.external_urls.spotify
}
}])
.concat(state.contents.slice(action.meta.index + 1))
}

Related

Updating state inside nested array when mapping the parent

I'm trying to update state inside a nested array with a map function and spread operator, but i don't understand how to get the key in the key/value pair to select a nested object.. There is an arrow in the code to the problematic part.
export default class ControlPanel extends Component {
state = {
words:
[
{
word: "a",
id: 1,
column: 1,
synonymns: {
all:[],
selected:[],
noneFound: false
}
}
]
}
}
updateSynonymnState = (wordId, theSynonymns) => {
const { words } = this.state
const newWords = words.map(word => {
if(word.id == wordId){
return {...word, synonymns.all: theSynonymns} //<--- synonymns.all is throwing an error, but that is the key that i need.
} else {
return word
}
})
this.setState ({words: newWords})
}
I could map the synonymns from the start but then I would loose the Id of the word which i need to select the right word..
How can I set synonymns.all = theSynonymns inside that words.map function, or is there a different way i should be able to set a nested key/value pair when mapping the parent parameter?
Immutable logic with nested values can get very tricky to get right. There are plenty of libraries that focus on that as well. For example: Immer, immutability-helper, immutable-js, and many more.
If you don't want to use another library for your state transitions, then you have to do a bit more work. You need to spread out each of the pieces of state from main object to the part you are modifying.
if (word.id == wordId) {
return { ...word, synonyms: { ...word.synonyms, all: theSynonymns } }; //<--- synonymns.all is throwing an error, but that is the key that i need.
} else {
return word;
}
You have to spread synonymns inside of word as well. Here is an example syntax of doing this:
const words = [{
word: "a",
id: 1,
column: 1,
synonymns: {
all: [],
selected: [],
noneFound: false
}
}]
const theSynonymns = ['b'];
const newWords = words.map(word => {
return {
...word,
synonymns: {
...word.synonymns,
all: theSynonymns
}
}
})
console.log(newWords)

How to use Lodash _.find() and setState() to change a value in the state?

I'm trying to use lodash's find method to determine an index based on one attribute. In my case this is pet name. After that I need to change the adopted value to true using setState. The problem is however; I do not understand how to combine setState and _.find()
As of right now I have this written. My main issue is figuring out how to finish this.
adopt(petName) {
this.setState(() => {
let pet = _.find(this.state.pets, ['name', petName]);
return {
adopted: true
};
});
}
This does nothing at the moment as it is wrong, but I don't know how to go from there!
In React you usually don't want to mutate the state. To do so, you need to recreate the pets array, and the adopted item.
You can use _.findIndex() (or vanilla JS Array.findIndex()) to find the index of the item. Then slice the array before and after it, and use spread to create a new array in the state, with the "updated" item:
adopt(petName) {
this.setState(state => {
const petIndex = _.findIndex(this.state.pets, ['name', petName]); // find the index of the pet in the state
return [
...state.slice(0, petIndex), // add the items before the pet
{ ...state[petIndex], adopted: true }, // add the "updated" pet object
...state.slice(petIndex + 1) // add the items after the pet
];
});
}
You can also use Array.map() (or lodash's _.map()):
adopt(petName) {
this.setState(state => state.map(pet => pet.name === petName ? ({ // if this is the petName, return a new object. If not return the current object
...pet,
adopted: true
}) : pet));
}
Change your adopt function to
adopt = petName => {
let pets = this.state.pets;
for (const pet of pets) {
if (!pet.adopted && pet.name === petName) {
pet.adopted = true;
}
}
this.setState({
pets
});
};
// sample pets array
let pets = [
{
name: "dog",
adopted: false
},
{
name: "cat",
adopted: false
}
]

Count arrays with values in state in React

I currently have the following state:
this.state = {
selectProduct: [somearrayValues],
quantityProduct: [],
colorsProduct: [somearrayValues],
stockProduct: [somearrayValues],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [somearrayValues],
attributeMetaProduct: [somearrayValues],
}
I do a fetch call to fill up the arrays with the needed data.
From here I need to get a count of Arrays that actually contain a value. I'm lost as how to accomplish this.
I first was trying to get to the state with a for each loop but I haven't even got passed this point:
let dropdownArrays = ...this.state;
dropdownArrays.forEach(function(element) {
console.log(element);
});
This gives me an error when babel attempts to compile.
I then tried the below, which returns nothing.
let dropdownArrays = [...this.state];
dropdownArrays.forEach(function(element) {
console.log(element);
});
I know I'm missing it so any help would be greatly appreciated.
Perhaps you could use the Object#values() method to access the array objects (ie the values) of the state, and then count the number of non-empty arrays like so:
// Pre-filled arrays with some values. This solution would work
// regardless of the values you populate the arrays with
const state = {
selectProduct: [1,2,3,4],
quantityProduct: [],
colorsProduct: [4,5,6,7],
stockProduct: [1,2],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [6,7,8,9,10],
attributeMetaProduct: [5,4,6],
}
const result = Object.values(state)
.filter((array) => array.length > 0)
.length;
console.log('Number of arrays in state with values (non-empty)', result)
Because state is an object, you instead could use a couple different options. You could do
this.state.values, which will return an array of the values in state.
this.state.values.forEach(function(value) {
console.log(value);
});
Or you could use this.state.entries, which will return an array of the key, value.
this.state.entries.forEach(function(entry) {
console.log(entry);
// expected output [key, value]
});
Lastly as you appear to already be attempting to use destructuring, you can also destructure the result.
this.state.entries.forEach(function([key, value]) {
console.log(key);
console.log(value);
});
you can try with https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
Object.entries(...this.state)[1].reduce((accumulator, currentValue) => accumulator + currentValue.length, 0)
Bear in mind that to use ...this.state you need https://babeljs.io/docs/en/babel-plugin-proposal-object-rest-spread
var state = {
selectProduct: [1,2,3,4],
quantityProduct: [],
colorsProduct: [4,5,6,7],
stockProduct: [1,2],
turnaroundProduct: [],
coatingProduct: [],
attributeProduct: [6,7,8,9,10],
attributeMetaProduct: [5,4,6],
}
// In your code replace state with this.state
let emptyArrays = Object.values(state).reduce(
(acc, current)=> ( !current.length? ++acc: acc),
0
)
console.log(emptyArrays)

Simple reducer accumulator should not be mutating the state, why is it?

For some reason my reducer is only returning a single element in the categories collection.
I'm just learning this accumlator logic, so I have kept it simple which I thought was suppose to simply return the exact same state as I started with.
My state in JSON looks like:
{
"account":{
"id":7,
"categories":[
{
"id":7,
"products":[
{
"productId":54
}
]
},
{
"id":9,
"products":[
{
"productId":89
}
]
}
]
}
}
In my reducer I have the following:
return {
...state,
account: {
...state.account,
categories: [state.account.categories.reduce((acc, cat) => {
return {...acc, ...cat}
}, {})]
}
};
When I output my state, I see that for some reason it has removed one of the categories from the collection.
Why isn't my state the same since the accumulator isn't filtering anything? It should be the exact same as it started with.
if you want to return categories unchanged (but with a different reference), you should do it like this:
categories: state.account.categories.reduce((acc, cat) => {
return [...acc, cat];
}, [])
In your code accumulator value is an object with categories props that is constantly overwritten by another item from an array so in the end only the last item is present.
Let's put aside react and redux and focus on reduce function. It takes an array and returns something different using the function called a reducer.
In the following example:
const array = [{num: 1}, {num: 2}];
you can take each of the elements of an array and merge their properties:
array.reduce((acc, item) => ({...acc, ...item}), {})
this is equal to
Object.assign({}, array[0], array[1]);
or
{...array[0], ...array[1]}
and result is {num: 2}, (first there was an empty object {}, then {num: 1} and finally {...{num: 1}, ...{num: 2}} gave {num: 2}
It doesn't matter if you enclose it in an array, it's still a one object created from merging all objects in the array together
If you want a copy of an array. This can be done like this:
array.reduce((acc, item) => [...acc, item], []);
This is equal to
[].concat(...array);

How to update object at specific index in array within React state?

I'm building a calorie counting application using React. One of my components has in its state a list of food items:
this.state = {
items: [
{
name: 'Chicken',
selectedServing: {
label: 'breast, grilled',
quantity: 3
}
},
{
name: 'French Fries',
selectedServing: {
label: 'medium container',
quantity: 1
}
}
]
When a user changes the serving size they consumed, I have to update the properties of the item in the items[] array. For example, if a user ate another chicken breast, I'd need to change the selectedServing object in items[0].
Since this array is part of the component's state, I'm using immutability-helper. I've found that I can properly clone and mutate the state in this way:
let newState = update(this.state, {
items: {
0: {
selectedServing: {
servingSize: {$set: newServingSize}
}
}
}
});
The above code sets the servingSize for the first element in the items[] array, which is Chicken. However, I won't know the index of the object I need to update beforehand, so the 0 I hardcoded won't work. It seems that I can't store this index in a variable, because update() will think it's an object key.
How can I programmatically update an object at a specific index in a list?
An variable can be used as a key of an object.
let foo = 3
let newState = { items: { [foo]: { somthing: 'newValue' } } }
// above is equal to { items: { '3': { somthing: 'newValue' } } }
You can find the index number of 'Chicken' and save it into an variable, and use it to composit newState.

Resources