2

I'm creating a dropdown using list items in React and am mapping through the data to get each option displayed. I want the user to be able to select multiple options and for all selected options to be saved in the state.

Currently the last option they select is being saved in an array in the state but I cannot get all of their selected options to be saved in the state because they aren't able to select multiple so they are only stored in the previous state.

How can I store multiple selected list items in an array in the state?

 const [selectedSymptoms, setSelectedSymptoms] = useState(null)

                       const handleSymptoms = (symptom) => {
                       setSelectedSymptoms([symptom.Name])
                    // selectedSymptoms !== null && selectedSymptoms.push(symptom.Name)
                       console.log(selectedSymptoms)
                       }

                            <button
                            className={styles.selectSymptomBtn}
                            type="button"
                            onClick={toggleSymptoms}
                        >
                            Select your symptom
                        </button>

                        <ul className={`${isSymptomsOpen ? styles.show : styles.hide}`}>
                            {data.symptoms.map((symptom) => (
                                <li onClick={(() => handleSymptoms(symptom))}>
                                    {symptom.Name}
                                </li>))}</ul>
RDev
  • 33
  • 4

2 Answers2

1

A Set would make the most sense here. This will prevent duplicates from being added if the user selects the same symptom twice, without having to search the entire array.

Passing a function to setSelectedSymptoms will give you access to the previous state.

const [selectedSymptoms, setSelectedSymptoms] = useState(new Set());

const handleSymptoms = (symptom) => setSelectedSymptoms((prev) => prev.add(symptom.Name));

Docs for Set: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set


You can expand this to easily deselect a symptom on a second click as well

  const handleSymptoms = (symptom) =>
    setSelectedSymptoms((prev) =>
      prev.delete(symptom.Name) ? prev : prev.add(symptom.Name)
    );

Edit


On rereading this I realized that solution is kind of halfway between immutable and mutable. You might as well go fully in one direction or the other.

Mutable solution - does not trigger rerender

  const selectedSymptoms = useRef(new Set());
  const handleSymptoms = (symptom) => selectedSymptoms.current.add(symptom.Name);

Immutable solution - triggers rerender

  const [selectedSymptoms, setSelectedSymptoms] = useState(new Set());
  const handleSymptoms = (symptom) =>
    setSelectedSymptoms((prev) => new Set(prev).add(symptom.Name));
Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • Thanks a lot! I just needed to add `selectedSymptoms.add(symptom.Name)` too before adding the previous symptoms with your code but this worked! @Chris Hamilton – RDev Aug 22 '22 at 19:06
  • Just FYI using a vanilla JS `Set` like this probably ***isn't*** preferred here since this code is mutating the state `Set` object and never returns a new `Set` reference for React to shallow diff on for reconciliation purposes. You might consider an [immutable Set](https://immutable-js.com/docs/v3.8.2/Set/) instead, which does meet this requirement. – Drew Reese Aug 22 '22 at 19:16
  • @DrewReese yeah I kinda just went halfway between mutable and immutable, I updated the answer to include both solutions. – Chris Hamilton Aug 22 '22 at 20:14
  • Updating a React ref won't trigger a rerender. – Drew Reese Aug 22 '22 at 20:18
  • @DrewReese noted – Chris Hamilton Aug 22 '22 at 20:47
0

The current logic is completely replacing the state value with an array of a single symptom name.

If you need to append, or remove, elements from the array then I suggest using a functional state update.

Example:

const [selectedSymptoms, setSelectedSymptoms] = useState([]);

...

const handleSymptoms = (symptom) => {
  setSelectedSymptoms(symptoms => {
    if (symptoms.includes(symptom.Name)) {
      // symptom name already selected, remove it
      return symptons.filter(name => name !== symptom.Name);
    } else {
      // symptom name non selected, add it
      return [...symptoms, symptom.Name];
    }
  });
};
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 1
    The other answer worked for me but thanks for your answer! @Drew Reese – RDev Aug 22 '22 at 19:08
  • 1
    @RDev Sure thing, glad to help. Cheers and good luck! Though I'd caution against using a Javascript Set as this doesn't create a shallow copy of the entire Set object which might cause rendering issues with React and its reconciliation process. You generally need to create new object references for ***all*** state, and nested state, that is being updated. The other solution here appears to actually be mutating the state reference, which should *always* be avoided. – Drew Reese Aug 22 '22 at 19:09