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
}
}
Related
So let's say i have a todoStore. It has an action that deletes a todo by id. Note that i tried both filter and splice:
export default class TodosStore {
constructor() {
makeAutoObservable(this)
}
todos = [
{
id: 1,
name: "name1",
completed: true
},
{
id: 15,
name: "name2",
completed: true
},
{
id: 14,
name: "name3",
completed: true
}
]
removeTodo(id) {
// this.todos = this.todos.filter(todo=>todo.id != id)
for (let todo of this.todos) {
if (todo.id == id) {
const indexOf = this.todos.indexOf(todo)
this.todos.splice(indexOf, 1)
}
}
}
};
The consuming Todos component(Note that i'm wrapping the Todo with observer):
import { combinedStores } from "."
const ObservableTodo = observer(Todo);
export default observer(() => {
const { todosStore } = combinedStores
return (
<div >
{todosStore.todos.map(todo=>{
return(
<ObservableTodo onDelete={()=>{todosStore.removeTodo(todo.id)}} onNameChange={(value)=>{todosStore.editTodoName(todo.id,value)}} key={todo.id} todo={todo}></ObservableTodo>
)
})}
</div>
)
})
The simple Todo component:
export default ({todo,onNameChange,onDelete}) => {
return (
<div style={{padding:'10px',margin:'10px'}}>
<p>ID: {todo.id}</p>
<input onChange={(e)=>{onNameChange(e.target.value)}} value={todo.name}></input>
<p>Completed: {todo.completed ? 'true' : 'false'} <button onClick={onDelete} className="btn btn-danger">Delete</button></p>
</div>
)
}
Even though i'm clearly mutating(as opposed to constructing a new array) the todos array within the store, Todos component rerenders(i see it via console.logs),
and so does every remaining Todo component.
Is there any way around it? Is there anything wrong with my setup perhaps? I'm using latest Mobx(6) and mobx-react.
Todos component is supposed to rerender because it depends on todos array content (because it map's over it). So when you change todos content by adding or removing some todo - Todos component will rerender because it needs to render new content, new list of todos.
Each single Todo rerenders because you have not wrapped it with observer. It is a good practice to wrap every component which uses some observable state, and Todo is clearly the one that does.
You change the length of the todo array, so the map function kicks in. Then while you are iterating over the elements, you are passing new properties to the ObservableTodo component (onDelete, onChange) this will make the ObservableTodo always rerender.
Even though the component is a Mobx observable, it still follows the "React rules", and when react sees new references in component properties, it will render the component.
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,
),
);
}
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.
I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}
I am wanting to change the colour of text when I click on it by triggering a function in a child element, such that it affects the state of the parent - As a particular parent state property determines whether the text is one colour or another.
I know that the function fired in the child DOES feed back up to the parent, as I can get the parent to console.log properly. However, I can't seem to get it to change the state property.
PS. I would like to use a "(prevState) =>" if this is applicable
Parent
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
class App extends React.Component {
constructor() {
super();
this.state = {
tasks
}
}
...
toggleTask(taskToToggle) {
const foundTask = tasks.find(task => task.name === taskToToggle.name)
foundTask.isComplete !== foundTask.isComplete;
this.setState({ tasks: this.state.tasks })
}
...
Child
return (
<div key={name} style={taskStyle} onClick={this.handleToggleComplete.bind(this)}>
{name}
</div>
)
handleToggleComplete() {
const taskToToggle = this.props;
this.props.toggleTask(taskToToggle);
}
You are returning the same array and task object.
So that current state tasks is the same array as next state tasks containing the same objects.
You need to return another array for example using Array.prototype.map
toggleTask(taskToToggle) {
this.setState(({tasks}) => ({
tasks: tasks.map(task =>
task.name === taskToToggle.name ? {...task, isComplete: !task.isComplete} : task)
}))
}
in your child onClick pass the this.props.toggleTask and bind the name the task directly to it like :
onClick={this.props.toggleTask.bind(name)}
And in your Parent in the function dont use taskToToggle.name but just "name" :
const foundTask = tasks.find(task => task.name === name)
Or you can just change completly the function to make it easier like :
toggleTask(name){
const updated = tasks.map(task => {
if(task.name === name){
task.isComplete = !task.isComplete
}
return task
})
this.setState({tasks: updated})
}
I think you need to calculate the final state before assigning the state
change your
this.setState({ tasks: this.state.tasks }) //doesn't change anything
to
let finalState = Object.assign({}, ...this.state.tasks, foundTask)
this.setState({ tasks: finalState }) //new object gets assigned to state
#Chris was correct in pointing out my n00bish error.
foundTask.isComplete !== foundTask.isComplete
Should have been
foundTask.isComplete = !foundTask.isComplete
This now works.