React:为带钩子的深度嵌套对象设置状态

IT技术 javascript reactjs state react-hooks setstate
2021-04-25 07:21:54

我正在 React 中使用深度嵌套的状态对象。我的代码库要求我们尽量坚持使用函数组件,因此每次我想更新嵌套对象内的键/值对时,我都必须使用钩子来设置状态。不过,我似乎无法了解更深层次的嵌套项目。我有一个带有 onChange 处理程序的下拉菜单。. .in 的 onChange 处理程序是一个内联函数,可以直接设置任何键/值对正在更改的值。

但是,我在每个内联函数中的展开运算符之后使用的语法是错误的。

作为一种解决方法,我已经将内联函数分解为它自己的函数,每次状态更改时都会重写整个状态对象,但这是非常耗时和丑陋的。我宁愿像下面这样内联:

 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>

上面代码的结果给了我一个“nestProp1”和“deepNestProp1”是未定义的,大概是因为它们永远不会被每个选择器到达/改变它们的状态。我的预期输出将是与选择器当前 val 的值匹配的选定选项(状态更改后)。任何帮助将不胜感激。

3个回答

我认为您应该使用 的功能形式setState,以便您可以访问当前状态并更新它。

喜欢:

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

看看是否有帮助:

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"/>

更新:带下拉菜单

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"/>

另一种方法是使用useReducer 钩子

const App = () => {      
  const reducer = (state, action) =>{
    return {...state, [action.type]: action.payload}
  }
  
  const [state, dispatch] = React.useReducer(reducer,{
    propA: 'foo1',
    propB: 'bar1'
  });
  
  const changeSelect = (prop, event) => {
    const newValue = event.target.value;
    dispatch({type: prop, payload: newValue});
  }
  
  return(
    <React.Fragment>
      <div>My nested state:</div>
      <div>{JSON.stringify(state)}</div>
      <select 
        value={state.propA} 
        onChange={(e) => changeSelect('propA', e)}
      >
        <option value='foo1'>foo1</option>
        <option value='foo2'>foo2</option>
        <option value='foo3'>foo3</option>
      </select>
      <select 
        value={state.propB} 
        onChange={(e) => changeSelect('propB', e)}
      >
        <option value='bar1'>bar1</option>
        <option value='bar2'>bar2</option>
        <option value='bar3'>bar3</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"/>

React state 的主要规则是不要直接修改 state这包括保存在顶级状态对象中的对象,或保存在其中的对象等。因此,要修改您的嵌套对象并使 React 可靠地处理结果,您必须复制您更改的每一层。(是的,真的。下面的详细信息,带有文档链接。)

另外,当您根据现有状态更新状态时,最好使用状态设置器的回调版本,因为状态更新可能是异步的(我不知道他们为什么说“可能在那里”,它们异步)和状态更新被合并,所以使用旧的状态对象会导致陈旧的信息被放回状态。

考虑到这一点,让我们看看您的第二个更改处理程序(因为它比第一个更深入),它需要 update stateObject.top_level_prop[0].nestprop4[0].deepNestProp1为了正确地做到这一点,我们必须复制我们正在修改的最深对象 ( stateObject.top_level_prop[0].nestprop4[0]) 及其所有父对象;其他对象可以重复使用。所以那是:

  • stateObject
  • top_level_prop
  • top_level_prop[0]
  • top_level_prop[0].nestprop4
  • top_level_prop[0].nestprop4[0]

那是因为它们都通过更改top_level_prop[0].nestprop4[0].deepNestProp1.

所以:

onChange={({target: {value}}) => {
    // Update `stateObject.top_level_prop[0].nestprop4[0].deepNestProp1`:
    setStateObject(prev => {
        // Copy of `stateObject` and `stateObject.top_level_prop`
        const update = {
            ...prev,
            top_level_prop: prev.top_level_prop.slice(), // Or `[...prev.top_level_prop]`
        };
        // Copy of `stateObject.top_level_prop[0]` and `stateObject.top_level_prop[0].nextprop4`
        update.top_level_prop[0] = {
            ...update.top_level_prop[0],
            nextprop4: update.top_level_prop[0].nextprop4.slice()
        };
        // Copy of `stateObject.top_level_prop[0].nextprop4[0]`, setting the new value on the copy
        update.top_level_prop[0].nextprop4[0] = {
            ...update.top_level_prop[0].nextprop4[0],
            deepNestProp1: value
        };
        return update;
    });
}}

最好不要复制树中未更改的其他对象,因为渲染它们的任何组件都不需要重新渲染,但需要复制我们正在更改的最深对象及其所有父对象。

围绕这一点的尴尬是useState在可能的情况下保持使用状态对象的原因之一

但我们真的必须这样做吗?

是的,让我们看一个例子。下面是一些没有做必要副本的代码:

请注意单击按钮似乎没有做任何事情(除了记录“已更改”),即使状态已更改。那是因为传递给的对象ShowName没有改变,所以ShowName没有重新渲染。

这是进行必要更新的一个:

该示例用于React.memo避免在其 props 未更改时重新渲染子组件。同样的事情发生在PureComponent或 任何实现shouldComponentUpdate并且在其props没有改变时不更新的组件

React.memo/ PureComponent/shouldComponentUpdate在主要代码库(和抛光组件)使用,以避免不必要的重新呈现。朴素的不完整状态更新会在使用它们时咬你,在其他时候也可能如此。