简体   繁体   English

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

[英]React: Setting State for Deeply Nested Objects w/ Hooks

I'm working with a deeply nested state object in React.我正在 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.我有一个带有 onChange 处理程序的下拉菜单。 . . .inside the onChange handler is an inline function to directly setValue of whatever key/val pair is changing. .in 的 onChange 处理程序是一个内联函数,可以直接设置任何键/值对正在更改的值。

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.上面代码的结果给了我一个“nestProp1”和“deepNestProp1”是未定义的,大概是因为它们永远不会被每个选择器到达/改变它们的状态。 My expected output would be the selected option matching the value of whatever the selector's current val is (after the state changes).我的预期输出将是与选择器当前 val 的值匹配的选定选项(状态更改后)。 Any help would be greatly appreciated.任何帮助将不胜感激。

I think you should be using the functional form of setState , so you can have access to the current state and update it.我认为你应该使用setState的函数形式,这样你就可以访问当前状态并更新它。

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

Another approach is to use the useReducer hook另一种方法是使用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"/>

The primary rule of React state is do not modify state directly . React state 的主要规则是不要直接修改 state That includes objects held within the top-level state object, or objects held within them , etc. So to modify your nested object and have React work reliably with the result, you must copy each layer that you change.这包括保存在顶级状态对象中的对象,或保存在其中的对象等。因此,要修改您的嵌套对象并使 React 可靠地处理结果,您必须复制您更改的每一层。 (Yes, really. Details below, with documentation links.) (是的,真的。下面的详细信息,带有文档链接。)

Separately, when you're updating state based on existing state, you're best off using the callback version of the state setter, because state updates may be asynchronous (I don't know why they say "may be" there, they are asynchronous) and state updates are merged , so using the old state object can result in stale information being put back in state.另外,当您根据现有状态更新状态时,最好使用状态设置器的回调版本,因为状态更新可能是异步的(我不知道他们为什么说“可能存在”,它们异步)和状态更新被合并,所以使用旧的状态对象会导致陈旧的信息被放回状态。

With that in mind, let's look at your second change handler (since it goes deeper than the first one), which needs to update stateObject.top_level_prop[0].nestprop4[0].deepNestProp1 .考虑到这一点,让我们看看您的第二个更改处理程序(因为它比第一个更深),它需要更新stateObject.top_level_prop[0].nestprop4[0].deepNestProp1 To do that properly, we have to copy the deepest object we're modifying ( stateObject.top_level_prop[0].nestprop4[0] ) and all of its parent objects;为了正确地做到这一点,我们必须复制我们正在修改的最深对象( stateObject.top_level_prop[0].nestprop4[0] )及其所有父对象; other objects can be reused.其他对象可以重复使用。 So that's:所以那是:

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

That's because they're all "changed" by changing top_level_prop[0].nestprop4[0].deepNestProp1 .那是因为它们都通过更改top_level_prop[0].nestprop4[0].deepNestProp1 “更改”。

So:所以:

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;
    });
}}

It's fine not to copy the other objects in the tree that aren't changing because any component rendering them doesn't need re-rendering, but the deepest object that we're changing and all of its parent objects need to be copied.最好不要复制树中未更改的其他对象,因为渲染它们的任何组件都不需要重新渲染,但需要复制我们正在更改的最深对象及其所有父对象。

The awkwardness around that is one reason for keeping state objects used with useState small when possible.围绕这一点的尴尬是在可能的情况下保持与useState使用的状态对象较小的原因之一。

But do we really have to do that?但我们真的必须这样做吗?

Yes, let's look at an example.是的,让我们看一个例子。 Here's some code that doesn't do the necessary copies:下面是一些没有做必要副本的代码:

 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>

Notice how clicking the button doesn't seem to do anything (other than logging "Changed"), even though the state was changed.请注意单击按钮似乎没有做任何事情(除了记录“已更改”),即使状态已更改。 That's because the object passed to ShowName didn't change, so ShowName didn't re-render.那是因为传递给ShowName的对象没有改变,所以ShowName没有重新渲染。

Here's one that does the necessary updates:这是进行必要更新的一个:

 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>

That example uses React.memo to avoid re-rendering child components when their props haven't changed.该示例使用React.memo来避免在子组件的 props 没有改变时重新渲染它们。 The same thing happens with PureComponent or any component that implements shouldComponentUpdate and doesn't update when its props haven't changed.同样的事情发生在PureComponent或任何实现shouldComponentUpdate并且在其 props 没有改变时不更新的组件。

React.memo / PureComponent / shouldComponentUpdate are used in major codebases (and polished components) to avoid unnecessary re-rendering. React.memo / PureComponent / shouldComponentUpdate用于主要代码库(和完善的组件)以避免不必要的重新渲染。 Naïve incomplete state updates will bite you when using them, and possibly at other times as well.朴素的不完整状态更新会在使用它们时咬你,在其他时候也可能如此。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM