I’m creating a simple to do list app in React. I’m encountering a problem where checking an item also checks the one below it in the list:
Even with a delay between checking the item & filtering the list:
Any ideas what could be causing this?
https://jsfiddle.net/yn03bx7w/
const {
useState,
useEffect,
} = React;
const {
Button,
Card,
CardActionArea,
CardContent,
Checkbox,
ThemeProvider,
Typography,
} = MaterialUI;
function Task(props) {
return (
<Card sx={{p: 1, m: 1, ...props.additionalStyles}}>
<CardActionArea>
<CardContent style={{padding: 2}}>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}}>
<Checkbox onChange={props.onChecked}/>
<div style={{display: 'flex', flexDirection: 'column'}}>
<Typography variant='body1' component='div' fontFamily='Segoe UI'>
{props.title}
</Typography>
</div>
</div>
</CardContent>
</CardActionArea>
</Card>
);
}
function App() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
setTasks([
{
title: "Task 1",
color: "red",
},
{
title: "Task 2",
},
{
title: "Task 3",
color: "blue",
},
]);
}, []);
const completeTask = (task) => {
task.isChecked = true;
setTasks(tasks.filter(t => !t.isChecked));
}
return (
<div style={{display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'center'}}>
<div style={{overflowY: 'scroll', width: '100%'}}>
{tasks.map(t =>
<Task title={t.title} onChecked={() => completeTask(t)} additionalStyles={{backgroundColor: t.color}}/>
)}
</div>
<Button variant='contained' fullWidth sx={{p: 1, m: 1}}>Add New</Button>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('container')
);
2
Answers
#1. key prop
The call to
tasks.map()
should be using uniquekey
props on the<Task>
component (React Docs about keys).#2. incorrect use of filter()?
This line of code…
…causes the Task containing the selected checkbox (let’s say Task #1) to disappear (because the call to
.filter()
filters it out). When Task #1 disappears, the one below it (Task #2) takes its place and acts as if it were the task that was checked. This happens because React has no way of knowing that the first Task in the DOM (after Task #1 disappears) is actually Task #2, not Task #1. React can distinguish Task #1 from Task #2 when the key prop is used.Well that is not really what is happening. Your code behavior is triggered by:
task.isChecked = true;
which you should never do as mentioned in react docs.What you should do instead:
const completeTask = (task) => setTasks(tasks.filter(t => t.title !== task.title))
WARNING: while either one of this condition would work by itself (without the other one), I really recommend not skipping either of them, since by doing so you might cause other unexpected behaviour in the future