I have a state object that looks like this:
PlaylistDictionary: {
someId: {
pages: [] // trying to push something to this array here
},
someId2: {
pages: [] // trying to push something to this array here
}, etc
}
I'm trying to update the "pages" array but I keep getting an error saying I'm mutating the state.. which I don't understand because if I'm making a copy of the state object with Object.assign... how am i mutating the state? thank you for any help
case types.ADD_PAGE_TO_PLAYLIST: {
let playlistDictCopy = Object.assign({}, state.playlistDict );
if ( !playlistDictCopy[ action.playlistId ].hasOwnProperty('pages') ) {
playlistDictCopy[ action.playlistId ].pages = [];
}
playlistDictCopy[ action.playlistId ].pages.push( action.pageId );
return Object.assign( {}, state, { playlistDict: playlistDictCopy } );
}
You're making a shallow copy of the state object, you are not copying any of the property values at all, including the array itself, so you are mutating the array.
Your .push call changes the array, and since you haven't created a new array, that will affect all previously stored state objects as well.
You can do
playlistDictCopy[ action.playlistId ].pages = playlistDictCopy[ action.playlistId ].pages.concat([action.pageId]);
or
playlistDictCopy[ action.playlistId ].pages = [ ...playlistDictCopy[ action.playlistId ].pages, action.pageId]);
to create a new array instead.
Related
import React, { useState, useEffect } from "react";
export const Test = () => {
const [data, setData] = useState([]);
const file = [{ id: 1, description: "Test Data" }];
useEffect(() => setData(file), []);
const manipulateData = (data) => {
let tempArray = [...data];
tempArray.map((item) => delete item.id);
};
return (
<>
<h1>Testing Data</h1>
{manipulateData(data)}
{console.log(data)}
</>
);
};
I want to make modification to the local variable "tempArray", and the changes shall not reflect in the variable "data"
This happens because you are creating a new array, but the objects in that array are not deeply copied. Instead, they represent just a reference to the very same objects as in data.
Because both references are pointing at the same object, if you delete an object property from the tempArray objects, you are actually deleting it from the data objects.
If you want to solve this, you have to destroy the reference by making a deep copy of the objects from the data array.
Here is an example how to do it:
export const Test = () => {
const [data, setData] = useState([{ id: 1, description: "Test Data" }]);
const manipulateData = (data) => {
let tempArray = data.map((item) => {item.description}); //this will create an array with new object, and destroy the reference to the objects of data.
};
return (
<>
<h1>Testing Data</h1>
{manipulateData(data)}
{console.log(data)}
</>
);
};
Also, instead of deleting a property, better pick the one that you need.
The following code:
let tempArray = [...data];
only shallow copies your data array, meaning that any of the objects inside of tempArray refer to the same objects in memory that are within the data array. This means that when you loop over your objects with .map(), each item refers to an object from data as well, which results in the objects in data changing when you use delete item.id.
Currently, you are using .map() just to loop over your tempArray, this isn't the purpose of .map(). The .map() method returns a new array, and the callback function you pass it should return a new value that you want to map the current value to. This means you can use .map() to create tempArray, by making the callback function return a new object that excludes the id property:
const manipulateData = (data) => {
const tempArray = data.map(({id, ...rest}) => rest);
// ... do what you need with `tempArray` ...
};
Above, {id, ...rest} is destructuring assignment syntax, which extracts the id property from the current element, and stores all other properties/values from the current object in rest. This allows you to create a new object that excludes the id property.
As a side note, you shouldn't be calling methods such as console.log() and manipulateData() inside of JSX if they don't return anything meaningful that will be rendered to the screen, instead, you can put these in a useEffect() hook.
const [data, setData] = useState([{ id: 1, description: "Test Data" }]);
try this
Your way is right in case you are not using a multi-dimensional array, but here you are cloning a multi-dimensional array,
and by using the = operator, it’ll assign objects/arrays by reference instead of by value! That means whenever you modify any of the two objects it's going to affect the value in both.
A good way to solve this is by creating a deep clone by doing something like this
const tempArray = JSON.parse(JSON.stringify([...data]))
tempArray.map((item) => delete item.id)
I'm fairly new to Redux.
My Web application is an eCommerce website where users have multiple carts (each with an id and name) and can place different items in each cart (inside an items array).
When a user deletes an item from the cart, my console.log() statements show that the carts array is being updated in the store, however the User interface doesn't reflect that unless i insert a new cart object inside of the carts array.
Why can't i update a nested array the same way i update a normal array in the store?
How do i fix this?
My initial Store
const intialStore = {
carts: [],
first_name : "ford",
last_name : "doly"
}
My Reducer Function
export default function reducer (store = intialStore, action) {
let {type, payload} = action;
switch(type) {
case DELETE_ITEM_IN_A_CART : {
let carts = [...store.carts]
let newCarts = carts.map((cartItem, index) => {
if (index == payload.cartIndex){
let array = [...cartItem.items]
array.splice(payload.itemIndex, 1)
cartItem.items = [...array ]
}
return cartItem ;
})
console.log(carts)
//carts[payload.cartIndex].items.splice(payload.itemIndex, 1)
return {...store, carts : newCarts}
}
default:
return {...store}
}
My Action Creator
export const deleteitemInCart = (cartIndex, itemIndex) => {
return {
type: DELETE_ITEM_IN_A_CART,
payload: {
cartIndex,
itemIndex
}
}
}
When you use the spread operator, you're only making a shallow copy. In your reducer, try using JSON.parse(JSON.stringify(carts)) in order to make a deep copy before performing the splice operation. This would ensure you are not mutating any existing state, but operating on a copy and returning a new copy at the end of the reducer.
map returns a new array, it does not mutate the original (which is good, we don't want to mutate it). But this means in order for your mapping actions to be persisted, you need to assign the result to a variable.
let newCarts = carts.map(...)
...
return {...store, carts: newCarts}
Right now, you're just returning the same carts array as the new state.
I'm learning Redux from this tutorial and I don't get how the spread operator below works in both the object and array. If ...state returns the same thing, how can it work in both situations? I thought it will just return an array, so it will work inside the SHUTTER_VIDEO_SUCCESS because it'll just spread whatever is inside the state into the new array in addition to the action.videos, but how will this work inside the SELECTED_VIDEO case? There is no key to place it in. The spread operator grabs the array not the key value pair from the default initialState right?
initialState.js
export default {
images: [],
videos: []
};
someComponent.js
import initialState from './initialState';
import * as types from 'constants/actionTypes';
export default function ( state = initialState.videos, action ) {
switch (action.type) {
case types.SELECTED_VIDEO:
return { ...state, selectedVideo: action.video }
case types.SHUTTER_VIDEO_SUCCESS:
return [...state, action.videos];
default:
return state;
}
}
UPDATE
Spread syntax allows you to spread an array into an object (arrays are technically objects, as is mostly everything in js). When you spread an array into an object, it will add a key: value pair to the object for each array item, where the key is the index and the value is the value stored at that index in the array. For example:
const arr = [1,2,3,4,5]
const obj = { ...arr } // { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5 }
const arr2 = [{ name: 'x' }, { name: 'y' }]
const obj2 = { ...arr2 } // { 0: { name: 'x' }, 1: { name: 'y' } }
You can also spread strings into arrays and objects as well. For arrays, it will behave similarly as String.prototype.split:
const txt = 'abcdefg'
const arr = [...txt] // ['a','b','c','d','e','f', 'g']
For objects, it will split the string by character and assign keys by index:
const obj = { ...txt } // { 0:'a',1:'b',2:'c',3:'d',4:'e',5:'f',6:'g' }
So you may be getting data that sort of works when you spread an array into an object. However, if the example you gave is what you're actually using, you're going to run into problems. See below.
=============
In the case of reducers in redux, when you use the spread syntax with an array it spreads each item from your array into a new array. It's basically the same as using concat:
const arr = [1,2,3]
const arr2 = [4,5,6]
const arr3 = [...arr, ...arr2] // [1,2,3,4,5,6]
// same as arr.concat(arr2)
With an object, the spread syntax spreads key: value pairs from one object into another:
const obj = { a: 1, b: 2, c: 3 }
const newObj = { ...obj, x: 4, y: 5, z: 6 }
// { a: 1, b: 2, c: 3, x: 4, y: 5, z: 6 }
These are two ways to help keep your data immutable in your reducers. The spread syntax copies array items or object keys/values rather than referencing them. If you do any changes in nested objects or objects in arrays, you'll have to take that into account to make sure you get new copies instead of mutated data.
If you have arrays as object keys then you can spread the entire object into a new one and then override individual keys as needed, including keys that are arrays that need updating with spread syntax. For example, an update to your example code:
const initialState = {
images: [],
videos: [],
selectedVideo: ''
}
// you need all of your initialState here, not just one of the keys
export default function ( state = initialState, action ) {
switch (action.type) {
case types.SELECTED_VIDEO:
// spread all the existing data into your new state, replacing only the selectedVideo key
return {
...state,
selectedVideo: action.video
}
case types.SHUTTER_VIDEO_SUCCESS:
// spread current state into new state, replacing videos with the current state videos and the action videos
return {
...state,
videos: [...state.videos, ...action.videos]
}
default:
return state;
}
}
This shows updating a state object and specific keys of that object that are arrays.
In the example you give, you're changing the structure of your state on the fly. It starts as an array, then sometimes returns an array (when SHUTTER_VIDEO_SUCCESS) and sometimes returns an object (when SELECTED_VIDEO). If you want to have a single reducer function, you would not isolate your initialState to just the videos array. You would need to manage all of your state tree manually as shown above. But your reducer should probably not switch the type of data it's sending back depending on an action. That would be an unpredictable mess.
If you want to break each key into a separate reducer, you would have 3 (images, videos and selectedVideo) and use combineReducers to create your state object.
import { combineReducers } from 'redux'
// import your separate reducer functions
export default combineReucers({
images,
videos,
selectedVideos
})
In that case each reducer will be run whenever you dispatch an action to generate the complete state object. But each reducer will only deal with its specific key, not the whole state object. So you would only need array update logic for keys that are arrays, etc.
According to the tutorial:
create-react-app comes preinstalled with babel-plugin-transform-object-rest-spread that lets you use the spread (…) operator to copy enumerable properties from one object to another in a succinct way. For context, { …state, videos: action.videos } evaluates to Object.assign({}, state, action.videos).
So, that's not a feature of ES6. It uses a plugin to let you use that feature.
Link: https://babeljs.io/docs/plugins/transform-object-rest-spread/
An array is also a key/value-pair but the key is an index. It's using ES6 destructuring and the spread syntax.
Redux docs on the subject
You may also want to read up on ES6 property value shorthand (or whatever it is called):
ES6 Object Literal in Depth
Whenever you find yourself assigning a property value that matches a property name, you can omit the property value, it’s implicit in ES6.
I have the following structure. I have a Company object which contains many Person objects in an array.
companies: CompanyInterface = {
persons: PersonInterface[] = []
}
I'm trying to show the persons that belongs to this company in a table. However, when I add/delete persons to the persons array, it should automatically be updated.
Right now my reducer looks like this:
// Initial state
companies: CompanyInterface[];
// Reducer
case Constants.GLOBAL_COMPANY_ADD_PERSON:
[action.payload.person].concat(state.companies.filter(company => company.id === action.payload.id)[0].persons));
return {
...state
};
Although I can verify concat works properly (I can see [Person] when I consolle log it) it doesn't update anything on the store.
So what is the best way to update an array in Redux, which is a property in the parent object?
Thank you.
You might want to take a look at Array.prototype.concat, as it does not mutate the array in place, but returns a new array. In your example you are not mutating the state.
Instead, you need to replace the old value with the new one. For instance:
case 'ADD_ELEMENT':
const { elements: previousElements } = state;
const newElement = action.payload;
const newElements = previousElements.concat(newElement)
return {
...state,
elements: newElements
};
Quick question guys, I am combining two objects using spread syntax, the new object is sorted automatically by keys of previous two objects, I dont want my new object to be sorted by keys (because I want users to see older conversations in my redux state above and newly fetched conversations). What can I do?
Here is my reducer, that takes array of conversations (array has 3 conversations first time around, and then +1 with another hit), and create objects based on conversation id
case actions.SET_CONVERSATION_MESSAGES: {
let convos = {};
payload.chatinbox.forEach(message => {
if (state[ message.chatsession_id ]) {
convos = Object.assign(convos, {
[ message.chatsession_id ]: {
messages: [...state[ message.chatsession_id ].messages, message]
}
});
} else if (!state[ message.chatsession_id ]) {
convos = Object.assign(convos, {
[ message.chatsession_id ]: {
messages: [message]
}
});
}
});
return {
...convos,
...state
};
}
here is how state object look
{
14632: {},
14652: {},
14741: {}
}
and when a new conversation comes in that has a key that fits in between
{
14542: {}
}
the new object get automatically sorted by them
{
14632: {},
14542: {},
14652: {},
14741: {}
}
and the result that I want is
{
14632: {},
14652: {},
14741: {},
14542: {}
}
for obvious reason, to show user what is fetched before and what is fetched afterwards, what can I do?
Objects don't guarantee order, so the keys can be shuffled around depending on browser implementation. If you want order to be maintained, consider using an array of objects with ids instead.
If you're using the IDs for direct access to the object, you might find array.find helpful if you transition to using an array of objects with IDs.