React: Setting State for Deeply Nested Objects w/ Hooks

snejame picture snejame · Sep 5, 2019 · Viewed 8.7k times · Source

I'm working with a deeply nested state object in React. My code base dictates that we try to stick with function components and so every time I want to update a key/value pair inside that nested object, I have to use a hook to set the state. I can't seem to get at the deeper nested items, though. I have a drop down menu w/ an onChange handler. . .inside the onChange handler is an inline function to directly setValue of whatever key/val pair is changing.

The syntax I'm using after the spread operator in each inline function is wrong, however.

As a workaround, I have resorted to factoring out the inline function to its own function that rewrites the entire state object every time the state changes, but that is extremely time consuming and ugly. I'd rather do it inline like the below:

 const [stateObject, setStateObject] = useState({

    top_level_prop: [
      {
        nestedProp1: "nestVal1",
        nestedProp2: "nestVal2"
        nestedProp3: "nestVal3",
        nestedProp4: [
          {
            deepNestProp1: "deepNestedVal1",
            deepNestProp2: "deepNestedVal2"
          }
        ]
      }
    ]
  });

<h3>Top Level Prop</h3>

   <span>NestedProp1:</span>
     <select
       id="nested-prop1-selector"
       value={stateObject.top_level_prop[0].nestedProp1}
       onChange={e => setStateObject({...stateObject, 
       top_level_prop[0].nestedProp1: e.target.value})}
     >
      <option value="nestVal1">nestVal1</option>
      <option value="nestVal2">nestVal2</option>
      <option value="nestVal3">nestVal3</option>
     </select>

<h3>Nested Prop 4</h3>

   <span>Deep Nest Prop 1:</span>
     <select
       id="deep-nested-prop-1-selector"
       value={stateObject.top_level_prop[0].nestprop4[0].deepNestProp1}
       onChange={e => setStateObject({...stateObject, 
       top_level_prop[0].nestedProp4[0].deepNestProp1: e.target.value})}
     >
      <option value="deepNestVal1">deepNestVal1</option>
      <option value="deepNestVal2">deepNestVal2</option>
      <option value="deepNestVal3">deepNestVal3</option>
     </select>

The result of the code above gives me a "nestProp1" and "deepNestProp1" are undefined, presumably because they are never being reached/having their state changed by each selector. My expected output would be the selected option matching the value of whatever the selector's current val is (after the state changes). Any help would be greatly appreciated.

Answer

cbdeveloper picture cbdeveloper · Sep 5, 2019

I think you should be using the functional form of setState, so you can have access to the current state and update it.

Like:

setState((prevState) => 
  //DO WHATEVER WITH THE CURRENT STATE AND RETURN A NEW ONE
  return newState;
);

See if that helps:

function App() {

  const [nestedState,setNestedState] = React.useState({
    top_level_prop: [
      {
        nestedProp1: "nestVal1",
        nestedProp2: "nestVal2",
        nestedProp3: "nestVal3",
        nestedProp4: [
          {
            deepNestProp1: "deepNestedVal1",
            deepNestProp2: "deepNestedVal2"
          }
        ]
      }
    ]
  });

  return(
    <React.Fragment>
      <div>This is my nestedState:</div>
      <div>{JSON.stringify(nestedState)}</div>
      <button 
        onClick={() => setNestedState((prevState) => {
            prevState.top_level_prop[0].nestedProp4[0].deepNestProp1 = 'XXX';
            return({
              ...prevState
            })
          }
        )}
      >
        Click to change nestedProp4[0].deepNestProp1
      </button>
    </React.Fragment>
  );
}

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

UPDATE: With dropdown

function App() {
  
  const [nestedState,setNestedState] = React.useState({
    propA: 'foo1',
    propB: 'bar'
  });
  
  function changeSelect(event) {
    const newValue = event.target.value;
    setNestedState((prevState) => {
      return({
        ...prevState,
        propA: newValue
      });
    });
  }
  
  return(
    <React.Fragment>
      <div>My nested state:</div>
      <div>{JSON.stringify(nestedState)}</div>
      <select 
        value={nestedState.propA} 
        onChange={changeSelect}
      >
        <option value='foo1'>foo1</option>
        <option value='foo2'>foo2</option>
        <option value='foo3'>foo3</option>
      </select>
    </React.Fragment>
  );
}

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