Here is my component:
After clicking delete, the checkbox underneath it becomes checked, even though it was never clicked.
Here is the parent and child component implementations (new to React)
TaskList.js
import { useState, useRef } from "react";
import Task from "./Task";
function TaskList({ className, tasks, setTasks }) {
let inputRef = useRef();
return (
<div className={className}>
<ul className="grow overflow-y-scroll">
{tasks.map((task, i) => (
<Task
description={task.description}
onChange={() => {
task.checked = !task.checked;
}}
/>
))}
</ul>
<input
className="my-2 border-b-2 border-blue-300"
placeholder="Enter task description..."
ref={inputRef}
></input>
<button
className="rounded-md bg-blue-400 w-full p-4 text-white text-2xl"
onClick={() => {
setTasks([
...tasks,
{ description: inputRef.current.value, checked: false },
]);
}}
>
Add
</button>
<button
className="w-full bg-red-600 rounded-md p-2"
onClick={() => {
setTasks(tasks.filter((t) => !t.checked));
}}
>
Delete
</button>
</div>
);
}
export default TaskList;
Task.js
import React, { useState } from "react";
function Task({ description, onChange }) {
return (
<li className="flex flex-row my-2">
<input className="mx-2" type="checkbox" onChange={onChange}></input>
<span>{description}</span>
</li>
);
}
CodePudding user response:
You're missing keys from the Task component, and you're using uncontrolled components, so when there's a re-render, you aren't telling React enough about which element that exists in the DOM corresponds to which <Task>
. The best fix would be to:
- Use controlled components instead of uncontrolled components; components are generally more predictable and useable with state, since the view should flow from the state. (If you use controlled components, you can also avoid from having to resort to a ref). Both the text input and the checkboxes can be made controlled.
- Use keys when returning from
.map
- Don't mutate state in React;
task.checked = !task.checked;
should be refactored.
Something along the lines of:
function TaskList({ className, tasks, setTasks }) {
const [value, setValue] = useState(''); // <-------------------
return (
<div className={className}>
<ul className="grow overflow-y-scroll">
{tasks.map((task, i) => (
<Task
key={task.description} // <-------------------
description={task.description}
checked={task.checked} // <-------------------
onChange={() => {
setTasks( // <-------------------
tasks.map(
(task, j) => i !== j ? task : { ...task, checked: !task.checked }
)
);
}}
/>
))}
</ul>
<input
className="my-2 border-b-2 border-blue-300"
placeholder="Enter task description..."
value={value} // <-------------------
onChange={(e) => setValue(e.currentTarget.value)} // <-------------------
></input>
<button
className="rounded-md bg-blue-400 w-full p-4 text-white text-2xl"
onClick={() => {
setTasks([
...tasks,
{ description: value, checked: false }, // <-------------------
]);
}}
>
...
function Task({ description, onChange, checked }) { // <-------------------
return (
<li className="flex flex-row my-2">
<input className="mx-2" type="checkbox" onChange={onChange} checked={checked} /> // <-------------------
<span>{description}</span>
</li>
);
}