datogio
datogio

Reputation: 351

How to make list of checkboxes controlled in loop in react

I want to make checkboxes controlled, handleChange does the job and toggles the completed property but react doesn't rerender the dom I suppose. How do I make it work?

import React from "react";

export default function App() {
  const [items, setItems] = React.useState([
    { title: "Item 1", completed: false, id: 1 },
    { title: "Item 2", completed: false, id: 2 },
    { title: "Item 3", completed: false, id: 3 }
  ]);

  function handleChange(index) {
    const itemsRef = items;
    itemsRef[index].completed = !itemsRef[index].completed;

    setItems(itemsRef);
  }

  return (
    <div id="app">
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              type="checkbox"
              checked={item.completed}
              onChange={() => handleChange(index)}
            />
            <label>{item.title}</label>
          </li>
        ))}
      </ul>
    </div>
  );
}

Upvotes: 0

Views: 865

Answers (1)

Asaf Aviv
Asaf Aviv

Reputation: 11800

useState is not like this.setState and will not re-render on mutations, you have to create a copy of the array before you mutate it

function handleChange(index) {
  const itemsRef = [...items];
  itemsRef[index].completed = !itemsRef[index].completed;
  setItems(itemsRef);
}

This is enough for React to re-render even if we mutate the inner object.

But, it is recommended to use the callback pattern and do a complete immutable update if you need to compute the next state based on the previous state because useState updates are always asynchronous

function handleChange(index) {
  setItems(prevState => prevState.map((obj, i) =>
    i === index 
      // create a new object and spread all of the properties
      // then toggle the completed value based on the previous state
      ? { ...obj, completed: !obj.completed }
      : obj
  ))
}

You can also write the above like this

function handleChange(index) {
  setItems(prevState => [
    ...prevState.slice(0, index),
    // create a new object and spread all of the properties
    // then toggle the completed value based on the previous state
    {
      ...prevState[index], 
      completed: !prevState[index].completed
    },
    ...prevState.slice(index + 1)
  ])
}

Upvotes: 2

Related Questions