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
};
Related
#interestingProblem can anybody explain, please 🤔
I had a problem while updating the state as in the first code block, but there was no problem when I updated the state as in the second code block as below.
I had a problem: (cannot assign to read-only property of object quantity)
const newItem = action.payload
newItem.quantity = 1
state.items = [...state.items, newItem]
I had no problem when I wrote the code like this
const newItem = action.payload
state.items = [...state.items, { ...newItem, quantity: 1 }]
the first approach you are mutating action.payload directly since you are not creating a copy to newItem but passing the same reference. Given action.payload is readonly you face the error:
// passing the same reference, 'newItem' points to 'action.payload'
// hence newItem is not copy
const newItem = action.payload
// here you mutate 'action.payload' since 'newItem' points to same reference
newItem.quantity = 1
state.items = [...state.items, newItem]
second approach works because you are creating a copy from action.payload not mutating it:
// here 'newItem' still points to same reference 'action.payload'
const newItem = action.payload
// but here you are spreading the values into a new object, not mutating directly
state.items = [...state.items, { ...newItem, quantity: 1 }]
instead you should create a copy first to your approach to work:
// here you create a new object from 'action.payload''action.payload'
// hence newItem contains the same values but it's a different object
const newItem = { ...action.payload }
// now you are not mutating 'action.payload', only 'newItem' that's a new object
newItem.quantity = 1
state.items = [...state.items, newItem]
action.payload might be a readonly object. On the second code block, the spread operator passes the key-value pairs to the new object.
Because when doing a like **kwargs with state in React I assume, you are passing a no nested state into one that has a nested state reassinging it to a non-nested stated breaking the goal of you're code.
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 have an array of 6 objects which have a uid and nothing else. This is so I can repeat over them and have some placeholder content until an object is ready to be added into the array. I set a unique key when a new object is selected. However if I select the same object twice, even though I'm setting a unique key. It seems to update the unique key on the duplicate item (even though the unique key is different).
Might be easier to see the code/app in action here, an example of the problem would be clicking squirtle then blastoise, take a note of the uid's shown. Then click squirtle again and for some reason it updates the old squirtle with the new squirtles uid causing a duplicate key error. https://codesandbox.io/s/l75m9z1xwq or see code below. Math.random is just placeholder until I can get this working correctly.
const initState = {
party: [
{ uid: 0 },
{ uid: 1 },
{ uid: 2 },
{ uid: 3 },
{ uid: 4 },
{ uid: 5 }
]
};
When I click on something this is triggered:
handleClick = pokemon => {
// setup a uid, will need a better method than math.random later
pokemon.uid = Math.random();
this.props.addToParty(pokemon);
};
This then calls a dispatch which triggers the following reducer. Which essentially just checks if the object has no normal ID then replace the content with the payload sent over. It does this but also somehow updates any previous objects with the same uid even though the if statement does not run against them.
const rootReducer = (state = initState, action) => {
if (action.type === "ADD_POKEMON") {
let foundFirstEmptyPoke = false;
const newArray = state.party.map((pokemon, index) => {
if (typeof pokemon.id === "undefined" && foundFirstEmptyPoke === false) {
foundFirstEmptyPoke = true;
pokemon = action.payload; // set the data to the first object that ios empty
}
// if we get to the last pokemon and it's not empty
if (index === 5 && foundFirstEmptyPoke === false) {
pokemon = action.payload; // replace the last pokemon with the new one
}
return pokemon;
});
return {
party: newArray
};
}
return state;
};
The problem here is that, when you click to select a pokemon, you mutate the data you retrieved from the API:
handleClick = pokemon => {
pokemon.uid = Math.random(); // HERE
this.props.addToParty(pokemon);
};
You actually mutate the react state. What you should do is clone your pokemon data object, add an uid to the clone you just generated and update your redux state with it:
handleClick = pokemon => {
this.props.addToParty({
...pokemon,
uid: Math.random()
});
};
That way, no references to the actual react state are kept. Because that was what was happening when you say it updates the old squirtle with the new squirtles uid. When you tried to add another pokemon, you updated the data you retrieved from your API which was also referenced from your first pokemon slot (from your redux state).
In react/redux it's always better to not mutate objects:
this.props.addToParty({...pokemon, uid: Math.random()});
You are mutating the state. Use spread syntax *** to copy the state before updating.
return {
...state,
party: newArray
}
I wrote simple reducer for User entity, and now I want to apply best practices for it, when switching action types and returning state. Just to mention, I extracted actions types in separate file, actionsTypes.js.
Content of actionsTypes.js :
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS';
export const ADD_USER_SUCCESS = 'ADD_USER_SUCCESS';
export const EDIT_USER_SUCCESS = 'EDIT_USER_SUCCESS';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
First question, is it mandatory to have actions types for FAILED case? For example, to add GET_USERS_FAILED and so on and handle them inside usersReducer?
Root reducer is:
const rootReducer = combineReducers({
users
});
There is code of usersReducer, and I put comments/questions inside code, and ask for answers (what are best practices to handle action types):
export default function usersReducer(state = initialState.users, action) {
switch (action.type) {
case actionsTypes.GET_USERS_SUCCESS:
// state of usersReducer is 'users' array, so I just return action.payload where it is array of users. Will it automatically update users array on initial state?
return action.payload;
case actionsTypes.GET_USER_SUCCESS:
// What to return here? Just action.payload where it is just single user object?
return ;
case actionsTypes.ADD_USER_SUCCESS:
// what does this mean? Can someone explain this code? It returns new array, but what about spread operator, and object.assign?
return [...state.filter(user => user.id !== action.payload.id),
Object.assign({}, action.payload)];
case actionsTypes.EDIT_USER_SUCCESS:
// is this ok?
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
case actionsTypes.DELETE_USER_SUCCESS:
// I'm not sure about this delete part, is this ok or there is best practice to return state without deleted user?
return [...state.filter(user => user.id !== action.user.id)];
default:
return state;
}
}
I'm not an experienced developer but let me answer your questions what I've learned and encountered up to now.
First question, is it mandatory to have actions types for FAILED case?
For example, to add GET_USERS_FAILED and so on and handle them inside
usersReducer?
This is not mandatory but if you intend to give a feedback to your clients it would be good. For example, you initiated the GET_USERS process and it failed somehow. Nothing happens on client side, nothing updated etc. So, your client does not know it failed and wonders why nothing happened. But, if you have a failure case and you catch the error, you can inform your client that there was an error.
To do this, you can consume GET_USERS_FAILED action type in two pleases for example. One in your userReducers and one for, lets say, an error or feedback reducer. First one returns state since your process failed and you can't get the desired data, hence does not want to mutate the state anyhow. Second one updates your feedback reducer and can change a state, lets say error and you catch this state in your component and if error state is true you show a nice message to your client.
state of usersReducer is 'users' array, so I just return
action.payload where it is array of users. Will it automatically
update users array on initial state?
case actionsTypes.GET_USERS_SUCCESS:
return action.payload;
This is ok if you are fetching whole users with a single request. This means your action.payload which is an array becomes your state. But, if you don't want to fetch all the users with a single request, like pagination, this would be not enough. You need to concat your state with the fetched ones.
case actionsTypes.GET_USERS_SUCCESS:
return [...state, ...action.payload];
Here, we are using spread syntax.
It, obviously, spread what is given to it :) You can use it in a multiple ways for arrays and also objects. You can check the documentation. But here is some simple examples.
const arr = [ 1, 2, 3 ];
const newArr = [ ...arr, 4 ];
// newArr is now [ 1, 2, 3, 4 ]
We spread arr in a new array and add 4 to it.
const obj = { id: 1, name: "foo, age: 25 };
const newObj = { ...obj, age: 30 };
// newObj is now { id: 1, name: "foo", age: 30 }
Here, we spread our obj in a new object and changed its age property. In both examples, we never mutate our original data.
What to return here? Just action.payload where it is just single user
object?
case actionsTypes.GET_USER_SUCCESS:
return ;
Probably you can't use this action in this reducer directly. Because your state here holds your users as an array. What do you want to do the user you got somehow? Lets say you want to hold a "selected" user. Either you can create a separate reducer for that or change your state here, make it an object and hold a selectedUser property and update it with this. But if you change your state's shape, all the other reducer parts need to be changed since your state will be something like this:
{
users: [],
selectedUser,
}
Now, your state is not an array anymore, it is an object. All your code must be changed according to that.
what does this mean? Can someone explain this code? It returns new
array, but what about spread operator, and object.assign?
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), Object.assign({}, action.payload)];
I've already tried to explain spread syntax. Object.assign copies some values to a target or updates it or merges two of them. What does this code do?
First it takes your state, filters it and returns the users not equal to your action.payload one, which is the user is being added. This returns an array, so it spreads it and merges it with the Object.assign part. In Object.assign part it takes an empty object and merges it with the user. An all those values creates a new array which is your new state. Let's say your state is like:
[
{ id: 1, name: "foo" },
{ id: 2, name: "bar" },
]
and your new user is:
{
id: 3, name: "baz"
}
Here what this code does. First it filters all the user and since filter criteria does not match it returns all your users (state) then spread it (don't forget, filter returns an array and we spread this array into another one):
[ { id: 1, name: "foo"}, { id: 2, name: "bar" } ]
Now the Object.assign part does its job and merges an empty object with action.payload, a user object. Now our final array will be like this:
[ { id: 1, name: "foo"}, { id: 2, name: "bar" }, { id: 3, name: "baz" } ]
But, actually Object.assign is not needed here. Why do we bother merging our object with an empty one again? So, this code does the same job:
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), action.payload ];
is this ok?
case actionsTypes.EDIT_USER_SUCCESS:
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
It seems ok to me. You don't mutate the state directly, use spread syntax to create a new one, update the related part and finally set your state with this new one.
I'm not sure about this delete part, is this ok or there is best
practice to return state without deleted user?
case actionsTypes.DELETE_USER_SUCCESS:
return [...state.filter(user => user.id !== action.user.id)];
Again, it seems ok to me. You filter the deleted user and update your state according to that. Of course there are other situations you should take into considiration . For example do you have a backend process for those? Do you add or delete users to a database? If yes for all the parts you need to sure about the backend process success and after that you need to update your state. But this is a different topic I guess.
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.