How to update nested object in redux - reactjs

I have an object:
{ //birthdaysObject
'2000':{
'January':
[{
name: 'Jerry'
},
{
name: 'Chris'
}]
},
'2001':{
'February':
[{
name: 'John'
}]
}
When I go to update the redux store it is replacing the entire year (eg. '2000') object with the new one that I send to my reducer.
How can I push the the nested array of objects without replacing the entire year object?
My reducer currently looks like:
return Object.assign({}, state, {
...state,
birthdays: Object.assign({}, state.birthdays, {
...state.birthdays,
...birthdays
})
});
Where ...birthdays would be another object in the same format as the first code snippet.
I am also open to suggestions about the structure of my data, normalizing, etc.
Any help would be appreciated.
The object keys in the birthdaysObject are unknown and are assigned when iterating through a separate object. I've tried kolodny/immutability-helper however the $merge function is returning the same results as what my reducer is already doing.

I had the same problem some time ago.
Follow the way I done it.
You have an object, but I think you should have an array of objects.
I also have different names on variables, but this should not be a problem to understand the logic
//do a copy of the array first
let newSubscriptions = state.customer.master.subscriptions.slice();
//for the value you want to change, find it's position in the array first
const indexInSubscriptions = newSubscriptions.map(function(item) {
return item.id;
}).indexOf( action.id);
//get the child you want to edit and keep it in a new variable
const under_edit_subscription = state.customer.master.subscriptions[indexInSubscriptions];
//go again over the array and where is the value at the index find above, replace the value
newSubscriptions = newSubscriptions.map((item, i) =>
i === indexInSubscriptions ? under_edit_subscription : item
)
//add the whole array into the state
return {
...state,
customer: {
...state.customer,
master: {
...state.customer.master,
subscriptions : newSubscriptions
}
}
}

Related

How to organise my nested object for better state management?

This is more of an organisation than technical question. I think I may be adding complexity, where a more experienced dev would simplify. I lack that experience, and need help.
It's a menu editor, where I load a menu object from my database into state:
state = {
user_token: ####,
loadingMenu: true,
menu: {} // menu will be fetched into here
}
The object looks like this:
{
menuID: _605c7e1f54bb42972e420619,
brandingImg: "",
specials: "2 for 1 drinks",
langs: ["en", "es"],
items: [
{
id: 0,
type: "menuitem",
isVisible: true,
en: {
name: "sandwich 1",
desc: "Chicken sandwish"
},
es: {
name: "torta 1"
},
price: 10
},
// ...
// ABOUT 25 MORE ITEMS
]
}
The UI allows user to click on and update the items individually. So when they change the text I find myself having to do weird destructuring, like this:
function reducer(state, action) {
if (action.type === UPDATE_NAME) {
const newMenuItems = state.menu.items.map((oldItem) => {
if (oldItem.id === action.payload.id) {
return { ...oldItem, ["en"]: { ...oldItem["en"], name: action.payload.newName } }
// ["en"] for now, but will be dynamic later
}
return oldItem
})
return { ...state, menu: { ...state.menu, items: newMenuItems } }
}
}
This seems like a a bad idea, because I'm replacing the entirety of state with my new object. I'm wondering if there is a better way to organize it?
I know there are immutability managers, and I tried to use immer.js, but ran into an obstacle. I need to map through all my menu items to find the one user wants to edit (matching the ID to the event target's ID). I don't know how else to target it directly, and don't know how to do this:
draft.menu.items[????][lang].name = "Sandwich One"
So again, I'm thinking that my organisation is wrong, as immutability managers should probably make this easy. Any ideas, what I can refactor?
First of all, your current reducer looks fine. That "weird destructuring" is very typical. You will always replace the entirety of state with a new object, but you are dealing with shallow copies so it's not an entirely new object at every level. The menu items which you haven't modified are still references to the same objects.
I need to map through all my menu items to find the one user wants to edit (matching the ID to the event target's ID). I don't know how else to target it directly.
You would use .findIndex() to get the index of the item that you want to update.
const {lang, name, id} = action.payload;
const index = draft.menu.items.findIndex( item => item.id === id);
if ( index ) { // because there could be no match
draft.menu.items[index][lang].name = name;
}
This is more of an organisation than technical question. I think I may be adding complexity, where a more experienced dev would simplify. I lack that experience, and need help.
My recommendation for the state structure is to store all of the items in a dictionary keyed by id. This makes it easier to update an item because you no longer need to find it in an array.
const {lang, name, id} = action.payload;
draft.items[index][lang].name = name;
The menu object would just have an array of ids instead of an array of objects for the items property. When you select a menu, your selector can replace the ids with their objects.
const selectMenu = (state) => {
const menu = state.menu;
return { ...menu, items: menu.items.map((id) => state.items[id]) };
};

How to update an object in an array inside store? [duplicate]

I have a todo list and want to set the state of that item in the array to "complete" if the user clicks on "complete".
Here is my action:
export function completeTodo(id) {
return {
type: "COMPLETE_TASK",
completed: true,
id
}
}
Here is my reducer:
case "COMPLETE_TASK": {
return {...state,
todos: [{
completed: action.completed
}]
}
}
The problem I'm having is the new state does no longer have the text associated of that todo item on the selected item and the ID is no longer there. Is this because I am overwriting the state and ignoring the previous properties? My object item onload looks like this:
Objecttodos: Array[1]
0: Object
completed: false
id: 0
text: "Initial todo"
__proto__: Object
length: 1
__proto__: Array[0]
__proto__: Object
As you can see, all I want to do is set the completed value to true.
You need to transform your todos array to have the appropriate item updated. Array.map is the simplest way to do this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo => todo.id === action.id ?
// transform the one with a matching id
{ ...todo, completed: action.completed } :
// otherwise return original todo
todo
)
};
There are libraries to help you with this kind of deep state update. You can find a list of such libraries here: https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities
Personally, I use ImmutableJS (https://facebook.github.io/immutable-js/) which solves the issue with its updateIn and setIn methods (which are more efficient than normal objects and arrays for large objects with lots of keys and for arrays, but slower for small ones).
New state does no longer have the text associated of that todo item on
the selected item and the ID is no longer there, Is this because I am
overwriting the state and ignoring the previous properties?
Yes, because during each update you are assigning a new array with only one key completed, and that array doesn't contain any previous values. So after update array will have no previous data. That's why text and id's are not there after update.
Solutions:
1- Use array.map to find the correct element then update the value, Like this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: action.completed } : todo
)
};
2- Use array.findIndex to find the index of that particular object then update that, Like this:
case "COMPLETE_TASK":
let index = state.todos.findIndex(todo => todo.id === action.id);
let todos = [...state.todos];
todos[index] = {...todos[index], completed: action.completed};
return {...state, todos}
Check this snippet you will get a better idea about the mistake you are doing:
let state = {
a: 1,
arr: [
{text:1, id:1, completed: true},
{text:2, id:2, completed: false}
]
}
console.log('old values', JSON.stringify(state));
// updating the values
let newState = {
...state,
arr: [{completed: true}]
}
console.log('new state = ', newState);
One of the seminal design principles in React is "Don't mutate state." If you want to change data in an array, you want to create a new array with the changed value(s).
For example, I have an array of results in state. Initially I'm just setting values to 0 for each index in my constructor.
this.state = {
index:0,
question: this.props.test[0].questions[0],
results:[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0]],
complete: false
};
Later on, I want to update a value in the array. But I'm not changing it in the state object. With ES6, we can use the spread operator. The array slice method returns a new array, it will not change the existing array.
updateArray = (list, index,score) => {
// updates the results array without mutating it
return [
...list.slice(0, index),
list[index][1] = score,
...list.slice(index + 1)
];
};
When I want to update an item in the array, I call updateArray and set the state in one go:
this.setState({
index:newIndex,
question:this.props.test[0].questions[newIndex],
results:this.updateArray(this.state.results, index, score)
});

Redux - returning an array instead of an object in the reducer

I'm trying to make a change on a page, once addListItem is ran an array called "list" that is actually a redux state, needs to be updated. I managed to update it, but instead of an array I return an object. I need to return an array instead, but I don't know how to refactor the code to make it do that.
/**
* Add Item
*/
case 'playlist/addListItem_success': {
return {
...state,
list: {
...state.list,
[action.meta.position]: {
...state.list[action.meta.position],
status: true
}
}
}
}
To return an array, you'd have to use the array-spread syntax (e.g. [...someArray]) instead of object spread, but you can't use that to update a particular index. With a map you can elegantly express what you need though:
return {
...state,
list: state.list.map((e, i) => i === action.meta.position ? {...e, status: true} : e)
};

Update nth item in array in state in parent from child in React?

In my top level component I have a function to update state. I pass this down to different child elements so that they can update my main state.
In my top level component:
updateValue(item, value) {
this.setState({[item]: parseInt(value)});
}
This has worked so far however now I need to update the nth item in an array.
My top level state is like this:
this.state = {
chosenExercises: [
'Bench press',
'Squat',
'Pull up'
]
};
And in my child component Im trying to do something like:
this.props.updateValue('chosenExercises'[1], 'New exercise');
So that my state would then be:
this.state = {
chosenExercises: [
'Bench press',
'New exercise',
'Pull up'
]
};
Am I going about this the correct way? Or should my state be key value pairs?
this.state = {
chosenExercises: {
0: 'Bench press',
1: 'New exercise',
2: 'Pull up'
}
};
This would potentially solve some of my problems of making the exercises easier to target but Im not sure which is best practice.
Since the chosenExercises can be multiple it makes sense to make it as an array, however you need to update your state differently. Instead of passing the index of the array element to update, you should actually make a copy of the array, update it in the child element and then send the updated array to the parent.
You could do something like:
In Child:
updateValue = (item, index, value) => {
let newValue = [...this.props[item].slice(0, index), value, ...this.props[item].slice(index + 1)];
this.props.updateValue(item, newValue);
}
The thing with this is that your state has to remain immutable so you have to provide a new Array to update in your state. So you'll end up with something like:
this.updateValue('chosenExercises', 1, 'New exercise');
updateValue(item, index, value) {
const newArray = this.state[item].slice();
newArray[index] = value;
this.setState({ [item]: newArray });
}
The array.slice() function creates a new Array, in which you update the value by its index. Afterwards you update your component state with the new array.
If you happen to do this more often, React created an immutability helper for these things. You can read more about it here. This would let you do something like:
import update from 'react-addons-update';
this.setState({
[item]: update(this.state[item], {[index]: {$set: value } })
});
It can be done with this in the top level component:
updateValue(item, value, options) {
if (options.isChosenExercises === true) {
this.setState((prevState) => {
let newchosenExercises = prevState.chosenExercises.slice();
newchosenExercises[item] = value;
return {chosenExercises: newchosenExercises};
});
} else {
this.setState({[item]: parseInt(value)});
}
}
For normal uses pass an empty object as the last parameter:
this.props.updateValue('setLength', e.target.value, {})}
But when you want to update the exercise array pass an object with isExercises set to true.
chooseThisExercise() {
this.props.updateValue(numberInTheArrayToChange, newExercise, {isChosenExercises: true});
}

React-Redux update state of immutable object with nested array, delete element from array

I have the following object, from which I want to remove one comment.
msgComments = {
comments: [
{ comment:"2",
id:"0b363677-a291-4e5c-8269-b7d760394939",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" },
{ comment:"1",
id:"e88f009e-713d-4748-b8e8-69d79698f072",
postId:"e93863eb-aa62-452d-bf38-5514d72aff39" }
],
email:"test#email.com",
id:"e93863eb-aa62-452d-bf38-5514d72aff39",
post:"test",
title:"test"
}
The action creator hits the api delete function with the commentId:
// DELETE COMMENT FROM POST
export function deleteComment(commentId) {
return function(dispatch) {
axios.post(`${API_URL}/datacommentdelete`, {
commentId
},{
headers: { authorization: localStorage.getItem('token') }
})
.then(result => {
dispatch({
type: DELETE_COMMENT,
payload: commentId
});
})
}
}
My api deletes the comment and I send the comment id to my Reducer, this is working fine to this point, api works and comment is deleted. The problem is updating the state in the reducer. After much trial and error at the moment I am trying this.
case DELETE_COMMENT:
console.log('State In', state.msgComments);
const msgCommentsOne = state.msgComments;
const msgCommentsTwo = state.msgComments;
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload );
const newComments = [
...msgCommentsTwo.data.comments.slice(0, deleteIndexComment),
...msgCommentsTwo.data.comments.slice(deleteIndexComment + 1)
];
msgCommentsOne.data.comments = newComments;
console.log('State Out', msgCommentsOne);
return {...state, msgComments: msgCommentsOne};
Both state in AND state out return the same object, which has the appropriate comment deleted which I find puzzling.
Also the component is not updating (when I refresh the comment is gone as a new api call is made to return the updated post.
Everything else seems to work fine, the problem seems to be in the reducer.
I have read the other posts on immutability that were relevant and I am still unable to work out a solution. I have also researched and found the immutability.js library but before I learn how to use that I wanted to find a solution (perhaps the hard way, but I want to understand how this works!).
First working solution
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
email: state.msgComments.data.email,
post: state.msgComments.data.post,
title: state.msgComments.data.title,
id: state.msgComments.data.id,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
Edit:
Second working solution
I have found a second far more terse solution, comments welcome:
case DELETE_COMMENT:
const deleteIndexComment = state.msgComments.data.comments
.findIndex(elem => elem.id === action.payload);
return {
...state, msgComments: {
data: {
...state.msgComments.data,
comments: [
...state.msgComments.data.comments.slice(0, deleteIndexComment),
...state.msgComments.data.comments.slice(deleteIndexComment + 1)
]
}
}
};
That code appears to be directly mutating the state object. You've created a new array that has the deleted item filtered out, but you're then directly assigning the new array to msgCommentsOne.data.comments. The data field is the same one that was already in the state, so you've directly modified it. To correctly update data immutably, you need to create a new comments array, a new data object containing the comments, a new msgComments object containing the data, and a new state object containing msgComments. All the way up the chain :)
The Redux FAQ does give a bit more information on this topic, at http://redux.js.org/docs/FAQ.html#react-not-rerendering.
I have a number of links to articles talking about managing plain Javascript data immutably, over at https://github.com/markerikson/react-redux-links/blob/master/immutable-data.md . Also, there's a variety of utility libraries that can help abstract the process of doing these nested updates immutably, which I have listed at https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md.

Resources