I'm trying to follow a React tutorial and have not used much Javascript before. As far as I can tell, my code is exactly the same as in the tutorial but it isn't working.
The goal is to change the state of a checkbox by updating the completed attribute of the todo object
My code for updating the state is below:
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
console.log(todo)
return todo
})
console.log(updatedTodos)
return {
todos: updatedTodos
}
})
}
the part that I don't understand is the console output:
The 'Wash the dishes' checkbox starts unchecked. I click on it, and it seems to correctly show that todo.completed switches to true. But when I log updatedTodos, it is false again.
What am I missing? Why does todos.completed get logged as true but show as false when in updatedTodos?
Issue
Looks like state mutation.
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed // <-- mutates todo object!!
}
console.log(todo)
return todo
})
console.log(updatedTodos)
return {
todos: updatedTodos
}
})
}
Solution
Shallow copy the todo object that is being updated as well.
React rerendering works by using a process called reconciliation that uses shallow reference equality to determine if a certain element needs to be rerendered or not. When you mutate an object but the reference is the same then React bails on rerendering that element (i.e. updating the DOM), so you get stuck with the stale UI.
handleChange(id) {
this.setState(prevState =>
prevState.todos.map(todo =>
todo.id === id
? {
...todo, // <-- spread into new object reference
completed: !todo.completed, // <-- update property
}
: todo,
),
);
}
Related
I am creating a todolist app using React. The data for the todos look like the following:
const todoData = [
{
id: 1,
text: "Empty bin",
completed: true
},
{
id: 2,
text: "Call mom",
completed: false
}
]
Now, I have an App component where I import that data and save it in state.
import todoData from "./todoData"
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todoData,
}
this.handleChange = this.handleChange.bind(this)
}
...
I also have an handleChange method which is supposed to change the value of the completed property to its inverse value. For example: for the todo with an id of 1, it's text value is "Empty Bin" and completed is true so by default the checkbox would be checked. However, when it is clicked, completed should be false and the checkbox should no longer be clicked. For some reason, this does not happen, so completed stays at its default boolean value and doesn't flip. So when a checkbox is clicked no change happens.
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
return {
todos: updatedTodos
}
})
}
After using console.log I realized that todo.completed is indeed being changed to its opposite value, but for some reason, it is not changed in updatedTodos even though in devtools map() says the value was updated when it return a new array which was stored in updatedTodos. Hence, the state does not change and the checkbox can't be clicked
The TodoItem functional component is in a separate file from the App component and contains the HTML for the the todo elements. It is shown below:
function TodoItem(props) {
return (
<div className="todo-item">
<input type="checkbox"
checked={props.task.completed}
onChange={() => props.handleChange(props.task.id)}/>
<p>{props.task.text}</p>
</div>
)
}
Also, the TodoItem was rendered in the App component
render() {
const todoArray = this.state.todos.map(task => <TodoItem key={task.id}
task={task} handleChange={this.handleChange}/>)
return (
<div className="todo-list">
{todoArray}
</div>
)
}
Looks like you are mutating the todo object inside handleChange function
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if (todo.id === id) {
//todo.completed = !todo.completed this is mutating
// in react mutating a object will result in unexpected results like this.
// so you have to create a new object based on the current todo and return it
return {
...todo,
completed: !todo.completed
}
}
return todo
})
return {
todos: updatedTodos
}
})
}
What's happening there?
Each object in the array is pointed to a memory location, basically if we change the object property values (for EX: completed), without changing the memory location it's mutating,
And by doing todo.completed = !todo.completed we directly change the value of completed property but the todo object still pointed to the same memory location, so we mutate the object and react will not respond to it, and now by doing this
return {
...todo, // <- create a new object based on todo
completed: !todo.completed // <- change the completed property
}
we create a new object based on the todo {...todo} (new object = new memory location), and we change the value of completed => {...todo, completed: !todo.completed}, since this is pointed to new memory location react will respond to the changes.
if you are not familiar with the spread operator (...), read it here, don't forget to check out Spread in object literals section there
You need to spread your objects then mutate particular field
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
My changeProductName function called setState which return a "mutated prevState". I pass the function to and call it in children component via ContextAPI. The function successfully updated a product name displayed in children and parent, but the parent did to fire a re-render. How does the parent updated the view without re-rendering? Can anyone explain what the prevState actually is in setState?
const App = () => {
const [products, setProducts] = useState(initialValues);
const changeProductName = (id, newName) => {
setProducts((prevState) => { //is preState a copy of state?
prevState.products.filter(
(product) => product.id === id
)[0].name = newName; //Mutates prevState
return prevState; //Did I return a new state?
});
};
useEffect(() =>
console.log("I would know when App re-renders")); //No re-render!
return (
<> //Some React Switch and Routers
<div>
{product.map(product=>product.name)} //Successfully Updated!
</div>
<ProductContext value={(products, changeProductName)}>
<ProductPage /> //call changeProductName and it works!
</ProductContext>
</>
);
};
If I change the function not touching prevState, the parent re-renders as expected. Is this method better?
//this will trigger parent re-render.
const changeProductName = (id, newName) => {
setProducts((prevState) => {
prevState.products.filter(
(product) => product.id === id
)[0].name = newName;
return prevState;
});
};
Can anyone explain what the prevState actually is in setState?
prevState is a reference to the previous state. It is not a copy of the state, it is a reference of the object that sits inside the state. So changing that object will not alter the object reference.
Therefore it should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState.
For example, if you do a check inside your changeProduct name like:
setProducts(prevState => {
prevState.filter(product => product.id == id)[0].name = newName;
console.log(prevState === products); // This will console true
return prevState;
});
Also, as you are using hooks, when you write setProducts((prevState) => { prevState.products}... the prevState itself is already the products. So you will get an undefined error in your example when trying to access .products.
So I would recommend you to do:
const changeProductName = (id, newName) => {
setProducts(prevProducts =>
prevProducts.map(product =>
product.id === id ? { ...product, name: newName } : product
)
);
};
.map will build a new array based on prevState, and change the name of the products that have the id called in the function.
As far as I know, mutating the state is generally a bad idea.
According to this answer, mutating the state may not result in a re-render, since the reference to the state object is not changed during the mutation.
I'd rather use some kind of redux-like immutable pattern:
const changeProductName = (id, newName) => {
setProducts((prevState) => (
prevState.map(product=>{
if(product.id!==id){
// name is not changed since the id does not match
return product;
} else {
// change it in the case of match
return {...product, name:newName}
}
}
)
}
I have a checkbox <input type='checkbox' onClick={ this.props.isTicked.bind(this,id)} />
I also have to do tasks:
state = {
todos: [
{
id:1,
title: 'Earn 10 lvl Faceit',
completed: false
},
{
id:2,
title: 'Achieve 10 badges',
completed: false
}
]
}
So id of checkbox equals id of its task. Then i want to make 'isTicked' function to make completed be an opposite value. I also have style for every task(that is different for true and false)
<div style ={this.TaskStyle()}>
<p>
<input type='checkbox' onClick={ this.props.isTicked.bind(this,id)} />
{title}
</p>
</div>
TaskStyle:
TaskStyle = () => {
if (this.props.task_value.completed) {
return {
backgroundColor: 'darkgreen',
fontFamily: 'Arial',
padding:'10px'
}
}
else {
return {
backgroundColor: 'gray',
fontFamily: 'Arial',
padding:'10px'
}
}
}
Back to the point, this is a working version of 'isTicked':
this.setState({todos: this.state.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})})
And this is mine and i dont know why it is not working....
isTicked = (id) => {
this.state.todos.forEach(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
});
}
Can someone help me?)
Well, firstly in your isTicked function, you're not setting the state like the previous method does.
Secondly, map returns a new Array which you san set as a newState while forEach does not. In fact that's the only difference between the two.
Now when you're iterating though forEach and directly changing the state, React doesn't have a way to know that the state has changed and it has to trigger the render function. Hence, you're not seeing the changes in the other case. On the other hand, in the former case, when you're modifying the state though setState, React re-renders the component in turn and you see the effect.
It's as simple as that :)
Do Not Modify State Directly For example, this will not re-render a
component:
// Wrong
this.state.comment = 'Hello';
// Instead, use setState():
// Correct
this.setState({comment: 'Hello'});
You might like to read this: do-not-modify-state-directly
You are manipulating state directly in this code:
isTicked = (id) => {
this.state.todos.forEach(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
});
}
which can introduce all kind of unwanted behaviour (=bugs) into your code, as it circumvents Reacts state management. The first implementation is better, but not perfect, as through some weird coincidence the state you are mapping over could change. It would be better to use the callback syntax of setState:
this.setState(prevState => {
const newState = prevState.todos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
});
return { ...prevState, newState };
});
This should ensure that no side effects occur. Maybe this article is helpful for you.
Also, map returns a new value with the mapped over values while forEach just maps over all values, returning nothing.
Problem
Every time I dispatch an action (ex. TOGGLE_TODO), the array re-renders even though only one of the state values changed
Code
List rendering
{
arr.length > 0 ?
arr.map(({ id, text } = {}) => {
return (
<TaskElement key={id}
text={text}
toggleTask={() => toggleTask(id)}
removeTask={() => removeTask(id)} />
)
}) :
// ...
}
Reducer
...
case 'TOGGLE_TASK':
return state.map(task => (
task.id === action.id ? {
...task,
checked: !task.checked
} : task
))
...
When I toggle a task there is a visible delay between updates. I've tried to fix this by making the TaskElement a PureComponent and even wrote my own shouldComponentUpdate using shallow equality but it still re-renders.
I am aware that mapping an array creates a completely new one in memory which invalidates the key prop causing the re-render. Is there any way to fix this.
Thanks in advance.
EDIT
Similiar question : Shouldn't Redux prevent re-rendering?
Keep/Make the TaskElement as PureComponent.
Inside the Reducer, only update the array item which requires a change.
Reducer
...
case 'TOGGLE_TASK':
let stateCopy = [...state];
const toggledItemIndex = state.findIndex((task) => task.id === action.id);
stateCopy[toggledItemIndex] = {
...stateCopy[toggledItemIndex],
checked: !stateCopy[toggledItemIndex].checked,
};
return stateCopy;
...
I have an array of boolean as a state in my component. If it is false, I want to set it as true.
this.state = {
checkedPos: []
}
handleChange(index, reaction) {
if (!this.state.checkedPos[index])
{
this.state.checkedPos[index] = true;
this.addReaction(reaction);
this.forceUpdate();
}
}
It works, but the only problem I encounter is that it show this warning:
Do not mutate state directly. Use setState()
So I tried changing it and putting it like this:
this.setState({
checkedPos[index]: true
})
But it does not compile at all.
One solution would be to map the previous array in your state. Since you should never modify your state without setState I will show you how you can use it in this solution :
handleChange(index) {
this.setState(prev => ({
checkedPos: prev.checkedPos.map((val, i) => !val && i === index ? true : val)
}))
}
Here, map will change the value of your array elements to true only if the previous value was false and if the index is the same as the one provided. Otherwise, it returns the already existing value.
You can then use the second argument of setState to execute your function after your value has been updated :
handleChange(index) {
this.setState(prev => ({
checkedPos: prev.checkedPos.map((val, i) => !val && i === index ? true : val)
}), () => {
this.addReaction(reaction);
})
}
You can use the functional setState for this.
this.setstate((prevState) => ({
const checkedPos = [...prevState.checkedPos];
checkedPos[index] = true
return { checkedPos };
}))