React:使用 Hooks 为深度嵌套的对象设置状态

React: Setting State for Deeply Nested Objects w/ Hooks

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

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

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

 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" 是未定义的,大概是因为它们永远不会 reached/having 它们的状态被每个选择器改变。我的预期输出将是与选择器当前 val 的值匹配的所选选项(在状态更改后)。任何帮助将不胜感激。

我认为你应该使用 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 hook

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 状态的主要规则是 do not modify state directly。这包括保存在顶级状态对象中的对象,或保存在 them 等中的对象。因此,要修改嵌套对象并让 React 可靠地处理结果,您必须复制每一层你改变。 (是的,真的。下面有详细信息,带有文档链接。)

另外,当您基于现有状态更新状态时,最好使用状态的回调版本 setter,因为 state updates may be asynchronous (I don't know why they say "may be" there, they are asynchronous) and state updates are merged,所以使用旧状态对象可以导致过时的信息被放回状态。

考虑到这一点,让我们看看您的第二个更改处理程序(因为它比第一个更深入),它需要更新 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 一起使用的状态对象的原因之一。

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

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

const {useState} = React;

const ShowNamed = React.memo(
    ({obj}) => <div>name: {obj.name}</div>
);

const Example = () => {
    const [outer, setOuter] = useState({
        name: "outer",
        middle: {
            name: "middle",
            inner: {
                name: "inner",
            },
        },
    });
    
    const change = () => {
        setOuter(prev => {
            console.log("Changed");
            prev.middle.inner.name = prev.middle.inner.name.toLocaleUpperCase();
            return {...prev};
        });
    };
    
    return <div>
        <ShowNamed obj={outer} />
        <ShowNamed obj={outer.middle} />
        <ShowNamed obj={outer.middle.inner} />
        <input type="button" value="Change" onClick={change} />
    </div>;
};

ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

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

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

const {useState} = React;

const ShowNamed = React.memo(
    ({obj}) => <div>name: {obj.name}</div>
);

const Example = () => {
    const [outer, setOuter] = useState({
        name: "outer",
        middle: {
            name: "middle",
            inner: {
                name: "inner",
            },
        },
    });
    
    const change = () => {
        setOuter(prev => {
            console.log("Changed");
            const update = {
                ...prev,
                middle: {
                    ...prev.middle,
                    inner: {
                        ...prev.middle.inner,
                        name: prev.middle.inner.name.toLocaleUpperCase()
                    },
                },
            };
            
            return update;
        });
    };
    
    return <div>
        <ShowNamed obj={outer} />
        <ShowNamed obj={outer.middle} />
        <ShowNamed obj={outer.middle.inner} />
        <input type="button" value="Change" onClick={change} />
    </div>;
};

ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

该示例使用 React.memo to avoid re-rendering child components when their props haven't changed. The same thing happens with PureComponent or any component that implements shouldComponentUpdate 并且在其 props 未更改时不会更新。

React.memo / PureComponent / shouldComponentUpdate 用于主要代码库(和完善的组件)以避免不必要的重新渲染。天真的不完整状态更新会在使用它们时困扰您,并且可能在其他时候也是如此。