Reputation: 326
I have this working todo list React app.
The state variable tasks
contains objects, with a string for the task, and done
, a boolean. I know how to add a class based on that boolean, but what I'm confused about is how to change that specific object's done
variable using a click event.
Here's App.js:
import React, { useState } from 'react';
import './App.css';
import v4 from 'uuid';
function App() {
const [ tasks, setTasks ] = useState([{task: 'Test task',
done: false},
{task: 'other task',
done: false},
{task: 'blah task',
done: false}])
const [ formVal, setFormVal ] = useState('');
const handleSubmit = e => {
e.preventDefault();
setTasks([...tasks, {task: formVal, done: false}]);
// Todo: clear form after submit
};
const handleChange = e => {
setFormVal(e.target.value);
}
const handleClick = e => {
// This is probably a good place to use useReduce
}
return (
<>
<h1>To Do List</h1>
<br />
<ol>
{
tasks.map(item => (
<li key={v4()} className={item.done ? 'done' : ''} onClick={handleClick}>{item.task}</li>
))
}
</ol>
<form onSubmit={handleSubmit}>
<label>Add a task </label>
<input type="text" placeholder='What to do?' onChange={handleChange}></input>
<input type="submit" />
</form>
</>
);
}
export default App;
Upvotes: 0
Views: 1232
Reputation: 202618
A good place to start is to create a curried handleClick
handler that takes an id and returns the function to be used as the callback. Use a functional state update to map previous state to a new array and toggle the done
value when the id matches.
const handleClick = id => e => {
setTasks(tasks => tasks.map(task => task.id === id ? {
...task,
done: !task.done,
} : task));
};
This requires your data to also have stable ids. As is, your code will generate new id/keys each time it is rendered, thus negating the purpose of react keys to begin with (i.e. each render cycle each array element has a new key and the entire array will be rerendered). Instead of generating the keys in the render array mapping, generate them when data is added to the tasks array.
Code
import React, { useState } from "react";
import "./App.css";
import v4 from "uuid";
function App() {
const [tasks, setTasks] = useState([
{ task: "Test task", id: 'xxxxxx:xxxx:xx', done: false }, // <-- data has id field
{ task: "other task", id: 'yyyyyyy:yyyy:yy', done: false },
{ task: "blah task", id: 'zzzzzzz:zzzz:zz', done: false }
]);
const [formVal, setFormVal] = useState("");
const handleSubmit = e => {
e.preventDefault();
setTasks([...tasks, { task: formVal, id: v4(), done: false }]); // <-- generate id when adding task
// Todo: clear form after submit
};
const handleChange = e => {
setFormVal(e.target.value);
}
const handleClick = id => e => {
setTasks(tasks =>
tasks.map(task =>
task.id === id
? {
...task,
done: !task.done
}
: task
)
);
};
return (
<>
<h1>To Do List</h1>
<br />
<ol>
{tasks.map(item => (
<li
key={item.id} // <-- use item.id as key
className={item.done ? "done" : ""}
onClick={handleClick(item.id)} // <-- pass item.id to handler
>
{item.task}
</li>
))}
</ol>
<form onSubmit={handleSubmit}>
<label>Add a task </label>
<input type="text" placeholder="What to do?" onChange={handleChange} />
<input type="submit" />
</form>
</>
);
}
export default App;
If you with to clear the input upon submission then make the input a controlled input, i.e. give it a value
, and in the submit handler simply reset the value
...
function App() {
...
const [formVal, setFormVal] = useState("");
const handleSubmit = e => {
e.preventDefault();
...
setFormVal(''); // <-- clear form value
};
...
return (
<>
...
<form onSubmit={handleSubmit}>
...
<input
type="text"
placeholder="What to do?"
onChange={handleChange}
value={formVal} // <-- set input value from state
/>
...
</form>
</>
);
}
export default App;
Upvotes: 2
Reputation: 457
map can take 2 arguments where the second one is the index of the element you can send the index and then use that to retrieve the element and update it.
here is the changes i have made
const handleClick = index => {
// This is probably a good place to use useReduce
console.log(index);
console.log(tasks[index]);
let newTasks = [...tasks];
newTasks[index] = { ...tasks[index], done: true };
console.log(newTasks);
setTasks(newTasks );
};
<ol>
{tasks.map((item, index) => (
<li
className={item.done ? "done" : ""}
`enter code here`onClick={() => handleClick(index)}
>
{item.task}
{/* to check the change */}
{String(item.done)}
</li>
))}
</ol>
Upvotes: 1
Reputation: 869
Give your task object an id or something to identify it, for the moment we can take the task name as a non repetable value.
Add a dataset
prop to your li
such as
<li data-task={item.task} onClick={handleClick} key={v4()}>
Then in your handleClick
you can access that with e.target.dataset.task
, now you have the task name, filter through the state and change the done
value!
Upvotes: 0