skip to Main Content

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:

enter image description here

Even with a delay between checking the item & filtering the list:

enter image description here

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. #1. key prop

    The call to tasks.map() should be using unique key props on the <Task> component (React Docs about keys).

    #2. incorrect use of filter()?

    This line of code…

    setTasks(tasks.filter(t => !t.isChecked));
    

    …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.

    Login or Signup to reply.
  2. I’m encountering a problem where checking an item also checks the one
    below it in the list:

    Any ideas what could be causing this?

    Well that is not really what is happening. Your code behavior is triggered by:

    1. You are directly trying to mutate a state when you do task.isChecked = true; which you should never do as mentioned in react docs.
    2. You are not using any key on your array rendering. You can read more about why react needs keys here (I would actually recommend the full article), but basically what is happening is that you trying to change an object that you hold in the React state and afterwards you remove that object from the list. However because you don’t have any unique keys in your rendering, reacts ‘thinks’ that first element from your array is checked, and because your original first element is removed combined with the fact that you don’t use keys triggers this behaviour.

    What you should do instead:

    1. You don’t really need to set anything to checked. You can just have something like const completeTask = (task) => setTasks(tasks.filter(t => t.title !== task.title))
    2. Add a key to your JSX elements directly inside the map(), so something like
    {tasks.map(t => 
      <Task key={t.title} title={t.title} onChecked={() => completeTask(t)} additionalStyles={{backgroundColor: t.color}}/>
    )}
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search