React.js | what is different beetween this two functions? - javascript

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.

Related

React did not mount option tag using setState

I have a problem were the react state did not extract the data in the react-bootstrap select form-control tag. Below are what I did so far:
state structure
this.state = {
... more list
list: [] // in this list, each item contains { id: 1, name: 'some name' }
}
Some class
componentDidMount() {
// ...api fetch and here
axios.get('https://api-server.com/test/on/something').then(response => {
this.setState(state => {
response.data['something'].map(g => {
return state.list.push(g);
})
}, () => {
console.log(this.state.list) // it did set the state
})
});
}
render()
<Form.Control as="select">
{
// the problem is here! can't extract the data
this.state.list.map(g => {
return <option value={g.id}>{g.name}</option>
})
}
</Form.Control>
I am not sure why it didn't display each data but I am certainly sure that it did set each item in the state correctly.
You're mutating the existing state state.list.push(g); which should never be done in React. it's not a good way.
Try something like this instead, cloning everything:
this.setState(prevState => ({ ...prevState, list: [ ...prevState.list, ...response.data['something'] ] }))
I just tried your API URL and it didn't return anything.
Can you check your console to see if it returns something?

Why is the attribute seemingly being reverted?

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

Checkbox not changing state in React component

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

How do I modify the value of an array of Bool in React?

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

Unable to change state property when clicking on child - React

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.

Categories

Resources