How to implement multiple checkbox using react hook
Asked Answered
M

6

19

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;
Manhattan answered 23/5, 2019 at 10:16 Comment(0)
E
37

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

Eulaliaeulaliah answered 23/5, 2019 at 10:31 Comment(6)
Will this kick off his render though? It seems like he's rendering using the checkedItems state variable, but his code looks like it's only updating the setCheckedItems state variable. I may be misunderstanding it thoughArvind
@Arvind if you run the linked demo you will clearly see the checked property corresponds to the state, i.e. the ckeckbox shows as checked in response to the state update.Eulaliaeulaliah
Ahhh I see it now, nice!Arvind
@Eulaliaeulaliah thanks for quick reply. using object this works. i was wondering why it does not work using Map.Manhattan
@Manhattan It may have something to do with React internals. I know for sure that Map and Set weren't working well when used in Redux store, which is why I have approached your problem from this angle. Not sure if there's an official source saying one shouldn't use them in React component state, but looking at the difference there doesn't seem to be any benefit to Map over Object in your case.Eulaliaeulaliah
issue is we dont want to set default to false, we want to set it dynamically based on value from the map...imagine a enable and disable button on a webpage...you want result to be dynamic based from api result and set the checked to whatever api result gives then you modify afterwardsBlodgett
O
5

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)}
    />
Osteitis answered 18/8, 2021 at 21:22 Comment(1)
like this, it worked!Glabrescent
S
2

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>
Sneer answered 23/5, 2019 at 10:55 Comment(0)
B
2

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));
Bourbon answered 18/9, 2019 at 7:52 Comment(1)
Yes, or use functions to update state: reactjs.org/docs/hooks-reference.html#functional-updatesOdilo
B
0

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>
    );
}```
Barbe answered 8/7, 2021 at 23:20 Comment(0)
H
0

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 }} />;
Hypallage answered 24/4, 2023 at 7:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.