Search code examples
javascriptreactjsreact-hookssetstate

How to set multiple states using react hooks


I am having a problem setting multiple states in one of my react components. My ultimate goal is this: I firstly want to set my 'casesData' state (see below) so that the value of 'selected' can be set to 'true' or 'false' on each subsequent click event. I would then like to map through 'casesData' to find where 'selected: true', and then add the value of 'case' to 'chosenCases'. So in the end 'chosenCases' might look like: ['case1', 'case2'] as long as their respective objects in 'casesData' are equal to 'selected: true'.

My initial state is set up like this:

const [chosenCases, setChosenCases] = useState([]);
const [casesData, setCasesData] = useState([
    {
        case: 'Case1',
        description: 'some description',
        selected: false,
    },
    {
        case: 'Case2',
        description: 'some description',
        selected: false,
    },
]);

I render content to the screen by mapping through my 'casesData' state. Then I call a function from an onClick to start updating my states:

return (
    <>
       {casesData.map(
           (
               casex  // casex because 'case' is a reserved word
           ) => {
               return (
                   <span
                       onClick={selectCase}
                       data-case={casex.case}
                   >
                       <p>{casex.case}</p>
                       <p>{casex.description}</p>
                   </span>
                );
           }
       )}
    </>
);

Here is the function that should update both of my states.

const selectCase = (event) => {
    console.log(event.target.getAttribute('data-case'));  // e.g. 'case2'

    setCasesData(
        [...casesData].map((object) => {
            if (
                object.case.toLowerCase() ===
                event.target.getAttribute('data-case').toLowerCase()
            ) {
                return {
                    ...object,
                    selected: !object.selected,
                };
            } else return object;
        })
    );

    let newArray = [];
    casesData.map((object) => {
        if (object.selected === true) {
            newArray.push(object.case.toLowerCase());
        }
    });
    setChosenCases(newArray);
};

If I log my states to the console after running the above function I see that 'casesData' gets updated correctly, but 'chosenCases' is always one step behind. For instance, 'chosenCases' remains empty on the first run of the function, but is then set on the second run (e.g. ['case2'] but the data will reflect the previous function call).

Any help would be much appreciated. I would like to know the most appropriate way to set this up.


Solution

  • The simplest thing to do would be to store the new casesData in a variable so you don't have to wait for a new render before you can see the new state:

    const selectCase = (event) => {
        console.log(event.target.getAttribute('data-case'));  // e.g. 'case2'
    
        const newCasesData = casesData.map((object) => 
            (object.case.toLowerCase() === event.target.getAttribute('data-case').toLowerCase())
                ? {
                    ...object,
                    selected: !object.selected,
                } 
                : object
        )
    
        setCasesData(newCasesData);
    
        setChosenCases(
            newCasesData
                .filter(obj => obj.selected)
                .map(obj => obj.case.toLowerCase())
        );
    };
    

    You might also want to reconsider needing derived state like chosenCases and just take care of figuring out the selected cases at render time. That will make your code easier to reason about since you don't have to worry about the disposition of two pieces of state.