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)
});