Search code examples
reactjsreduxstate

React State Updation Issue


My component's state is as below:

const [state, setState] = useState({
        teamMembersOptions: [],
        selectedTeamMember: {},
    });

teamMembersOptions are being mapped from the redux state teamMembersList as below:

    const teamMembersList = useSelector(state => state.get_all_team_members.team)

    useEffect(() => {
        if (teamMembersList)
            mapTeamMembers();
    }, [teamMembersList])

    const mapTeamMembers = () => {
        const teamMembers = [];
        teamMembersList.map(member => {
            const memberObject = {
                'value': member.id,
                'label': member.first_name.charAt(0).toUpperCase() + member.first_name.slice(1) + ' ' + member.last_name.charAt(0).toUpperCase() + member.last_name.slice(1)
            }
            if (member.is_leader == 1) {
                memberObject.label = memberObject.label + ' (owner)'
                setState({
                    ...state,
                    selectedTeamMember: memberObject
                })
            }
            teamMembers.push(memberObject)
        })
        setState({
            ...state,
            teamMembersOptions: teamMembers
        })
    }

The state variables of selectedTeamMember and teamMemberOptions are not updating, it keeps consoling empty state. Whenever I console the local array of teamMembers inside mapTeamMembers function, it logs all the values successfully teamMembersList from Redux also logs successfully that means teamMembersList and teamMembers are not empty. But the state is not updating. Why the setState statement inside mapTeamMembers function is not updating the state?


Solution

  • There are a number of things going on here and lot of them cause renders to trigger more renders which is why you are getting unexpected output.

    I have add useMemo() and useCallback() around the data and calculation method respectively, and added their return values to the dependency array for useEffect(). This is to avoid the useEffect dependencies change on every render.

    Calling setState() within the .map() function doesn't feel like the right choice either as each time it is called a render might occur, even though you are halfway through the mapping operation. Instead I suggest, and opted for, using .reduce() on the array and returning that result which can then be used to update the state within the useEffect hook.

    Have a look at the working code below and a sample output given the defined input from teamMembersList. Note: this doesn't use Redux in the example given that it more setup to prove the concept.

    import { useCallback, useEffect, useMemo, useState } from "react";
    
    export default function App() {
      const [state, setState] = useState({
        teamMembersOptions: [],
        selectedTeamMember: {}
      });
    
      const teamMembersList = useMemo(
        () => [
          { id: 1, first_name: "John", last_name: "Smith", is_leader: 0 },
          { id: 2, first_name: "Maggie", last_name: "Simpson", is_leader: 1 }
        ],
        []
      );
    
      const mapTeamMembers = useCallback(
        () =>
          teamMembersList.reduce(
            (acc, member) => {
              const memberObject = {
                value: member.id,
                label:
                  member.first_name.charAt(0).toUpperCase() +
                  member.first_name.slice(1) +
                  " " +
                  member.last_name.charAt(0).toUpperCase() +
                  member.last_name.slice(1)
              };
              if (member.is_leader === 1) {
                memberObject.label = memberObject.label + " (owner)";
                acc.leader = memberObject;
              }
              acc.teamMembers.push(memberObject);
              return acc;
            },
            {
              teamMembers: [],
              leader: ""
            }
          ),
        [teamMembersList]
      );
    
      useEffect(() => {
        if (teamMembersList) {
          const members = mapTeamMembers();
          setState({
            selectedTeamMember: members.leader,
            teamMembersOptions: members.teamMembers
          });
        }
      }, [teamMembersList, mapTeamMembers, setState]);
    
      return (
        <div>
          <pre>
            <code>{JSON.stringify(state, null, 4)}</code>
          </pre>
        </div>
      );
    }
    
    

    The above will render out:

    {
        "selectedTeamMember": {
            "value": 2,
            "label": "Maggie Simpson (owner)"
        },
        "teamMembersOptions": [
            {
                "value": 1,
                "label": "John Smith"
            },
            {
                "value": 2,
                "label": "Maggie Simpson (owner)"
            }
        ]
    }
    

    I'd consider splitting the state object into individual state items but that's really up to you and how you want to handle the data.