Nakesh
Nakesh

Reputation: 576

How to implement multiple checkbox using react hook

I want to implement multiple checkboxes on my HTML page using react-hook.

I tried implementing using this URL: https://medium.com/@Zh0uzi/my-concerns-with-react-hooks-6afda0acc672. In the provided link it is done using class component and working perfectly but whenever I am using React hook setCheckedItems to update checkbox checked status it's not re-rendering the view.

The very first time the view is rendering and console.log() is printing from Checkbox component. After clicking on checkbox function handleChange gets called and checkedItems updates the value but the view is not rendering again (no console.log() printing). And {checkedItems.get("check-box-1")} is also not printing any value.

Below is my sample code.

CheckboxExample :

import React, { useState } from 'react';
import Checkbox from '../helper/Checkbox';

const CheckboxExample = () => {
    const [checkedItems, setCheckedItems] = useState(new Map());

    const handleChange = (event) => {
        setCheckedItems(checkedItems => checkedItems.set(event.target.name, event.target.checked));
        console.log("checkedItems: ", checkedItems);
    }

    const checkboxes = [
        {
            name: 'check-box-1',
            key: 'checkBox1',
            label: 'Check Box 1',
        },
        {
            name: 'check-box-2',
            key: 'checkBox2',
            label: 'Check Box 2',
        }
    ];


    return (
        <div>
            <lable>Checked item name : {checkedItems.get("check-box-1")} </lable> <br/>
            {
                checkboxes.map(item => (
                    <label key={item.key}>
                        {item.name}
                        <Checkbox name={item.name} checked={checkedItems.get(item.name)} onChange={handleChange} />
                    </label>
                ))
            }
        </div>
    );
}
export default Example;

Checkbox:

import React from 'react';

const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => {
    console.log("Checkbox: ", name, checked);

  return (<input type={type} name={name} checked={checked} onChange={onChange} /> )
}
export default Checkbox;

Upvotes: 19

Views: 51788

Answers (6)

Pavel T
Pavel T

Reputation: 97

I'm using custom hook:

export const useCheckboxes = (): [number[], (i: number) => void] => {
  const [checkedItems, setCheckedItems] = useState<number[]>([]);
  
  const onCheckboxPress = useCallback((i: number) => {
      const selectedItemsSet = new Set(checkedItems);
      selectedItemsSet[!selectedItemsSet.has(i) ? 'add' : 'delete'](i);
      setCheckedItems(Array.from(selectedItemsSet));
    }, [checkedItems]);

  return [checkedItems, onCheckboxPress];
};

And the implementation:

const [checkedItems, onCheckboxPress] = useCheckboxes();

const renderItem = ({ item, index }) => <ListItem checked={checkedItems.includes(index)} />;

return <FlatList {...{ data, renderItem }} />;

Upvotes: 0

Matt Browne
Matt Browne

Reputation: 12429

As an alternative to Map, you might consider using a Set. Then you don't have to worry about initially setting every item to false to mean unchecked. A quick POC:

    const [selectedItems, setSelectedItems] = useState(new Set())

    function handleCheckboxChange(itemKey: string) {
        // first, make a copy of the original set rather than mutating the original
        const newSelectedItems = new Set(selectedItems)
        if (!newSelectedItems.has(itemKey)) {
            newSelectedItems.add(itemKey)
        } else {
            newSelectedItems.delete(itemKey)
        }
        setSelectedItems(newSelectedItems)
    }

...

    <input
        type="checkbox"
        checked={selectedItems.has(item.key)}
        onChange={() => handleCheckboxChange(item.key)}
    />

Upvotes: 5

user16410720
user16410720

Reputation: 1


export default function Input(props) {
    const {
        name,
        isChecked,
        onChange,
        index,
    } = props;

    return (
        <>
            <input
                className="popup-cookie__input"
                id={name}
                type="checkbox"
                name={name}
                checked={isChecked}
                onChange={onChange}
                data-action={index}
            />
            <label htmlFor={name} className="popup-cookie__label">{name}</label>
        </>
    );
}

const checkboxesList = [
    {name: 'essential', isChecked: true},
    {name: 'statistics', isChecked: false},
    {name: 'advertising', isChecked: false},
];

export default function CheckboxesList() {
    const [checkedItems, setCheckedItems] = useState(checkboxesList);

    const handleChange = (event) => {
        const newCheckboxes = [...checkedItems];
        newCheckboxes[event.target.dataset.action].isChecked = event.target.checked;
        setCheckedItems(newCheckboxes);
        console.log('checkedItems: ', checkedItems);
    };

    return (
        <ul className="popup-cookie-checkbox-list">
            {checkboxesList.map((checkbox, index) => (
                <li className="popup-cookie-checkbox-list__item" key={checkbox.name}>
                    <Input
                        id={checkbox.name}
                        name={checkbox.name}
                        isChecked={checkbox.isChecked}
                        onChange={handleChange}
                        index={index}
                    />
                </li>
            ))}
        </ul>
    );
}```

Upvotes: 0

pawel
pawel

Reputation: 36995

I don't think using a Map to represent the state is the best idea.
I have implemented your example using a plain Object and it works:

https://codesandbox.io/s/react-hooks-usestate-xzvq5

const CheckboxExample = () => {
  const [checkedItems, setCheckedItems] = useState({}); //plain object as state

  const handleChange = (event) => {
      // updating an object instead of a Map
      setCheckedItems({...checkedItems, [event.target.name] : event.target.checked });
  }

  useEffect(() => {
    console.log("checkedItems: ", checkedItems);
  }, [checkedItems]);  

  const checkboxes = [
      {
          name: 'check-box-1',
          key: 'checkBox1',
          label: 'Check Box 1',
      },
      {
          name: 'check-box-2',
          key: 'checkBox2',
          label: 'Check Box 2',
      }
  ];


  return (
      <div>
          <lable>Checked item name : {checkedItems["check-box-1"]} </lable> <br/>
          {
              checkboxes.map(item => (
                  <label key={item.key}>
                      {item.name}
                      <Checkbox name={item.name} checked={checkedItems[item.name]} onChange={handleChange} />
                  </label>
              ))
          }
      </div>
  );
}

EDIT:

Turns out a Map can work as the state value, but to trigger a re-render you need to replace the Map with a new one instead of simply mutating it, which is not picked by React, i.e.:

const handleChange = (event) => {
  // mutate the current Map
  checkedItems.set(event.target.name, event.target.checked)
  // update the state by creating a new Map
  setCheckedItems(new Map(checkedItems) );
  console.log("checkedItems: ", checkedItems);
}

but in this case, I think there is no benefit to using a Map other than maybe cleaner syntax with .get() and .set() instead of x[y].

Upvotes: 37

rodamn
rodamn

Reputation: 2211

As a supplement to using a single object to hold the state of numerous items, the updates will not occur as expected if updating multiple items within a single render. Newer commits within the render cycle will simply overwrite previous commits.

The solution to this is to batch up all the changes in a single object and commit them all at once, like so:

// An object that will hold multiple states
const [myStates, setMyStates] = useState({});

// An object that will batch all the desired updates
const statesUpdates = {};

// Set all the updates...
statesUpdates[state1] = true;
statesUpdates[state2] = false;
// etc...

// Create a new state object and commit it
setMyStates(Object.assign({}, myStates, statesUpdates));

Upvotes: 2

Francis Leigh
Francis Leigh

Reputation: 1960

Seems a bit of a long way round but if you spread the map out and apply it to a new Map your component will re-render. I think using a Object reference instead of a Map would work best here.

const {useState} = React

const Mapper = () => {
  const [map, setMap] = useState(new Map());

  const addToMap = () => {
    const RNDM = Math.random().toFixed(5)
    map.set(`foo${RNDM}`, `bar${RNDM}`);
    setMap(new Map([...map]));
  }

  return (
    <div>
      <ul>
        {[...map].map(([v, k]) => (
          <li key={k}>
            {k} : {v}
          </li>
        ))}
      </ul>
      <button onClick={addToMap}>add to map</button>
    </div>
  );
};

const rootElement = document.getElementById("react");
ReactDOM.render(<Mapper />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Upvotes: 2

Related Questions