【问题标题】:React: Setting State for Deeply Nested Objects w/ HooksReact:使用 Hooks 为深度嵌套对象设置状态
【发布时间】:2020-01-07 23:08:17
【问题描述】:

我正在使用 React 中深度嵌套的状态对象。我的代码库要求我们尝试使用函数组件,因此每次我想更新嵌套对象中的键/值对时,我都必须使用钩子来设置状态。不过,我似乎无法理解更深层次的嵌套项目。我有一个带有 onChange 处理程序的下拉菜单。 . .inside 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 的值匹配的选定选项(在状态更改之后)。任何帮助将不胜感激。

【问题讨论】:

标签: javascript reactjs state react-hooks setstate


【解决方案1】:

我认为您应该使用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"/>

【讨论】:

  • 这行得通!谢谢你。现在把它变成一个下拉列表。 . .
  • 您需要将其作为内联函数吗?将其设置为独立函数并使用选项值作为参数调用它更容易,不是吗?
  • 不一定。 . .但我需要能够运行带有事件的内联函数,而不是像他那样手动为其分配一个字符串值。现在想弄清楚。 . .
  • 你知道如何运行你引用的同一个函数,同时从下拉列表中传递一个事件吗?
  • 我添加了一个新示例。看看有没有帮助。
【解决方案2】:

另一种方法是使用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"/>

【讨论】:

    【解决方案3】:

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

    另外,当您根据现有状态更新状态时,最好使用状态设置器的回调版本,因为state updates may be asynchronous(我不知道他们为什么说“可能存在”,他们异步的)和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 来避免在子组件的道具未更改时重新渲染它们。 PureComponent 或任何实现 shouldComponentUpdate 并且在其 props 未更改时不会更新的组件也会发生同样的事情。

    React.memo / PureComponent / shouldComponentUpdate 用于主要代码库(和抛光组件),以避免不必要的重新渲染。幼稚的不完整状态更新会在使用它们时给您带来麻烦,并且可能在其他时候也是如此。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-02-21
      • 2021-09-03
      • 2017-08-27
      • 2018-04-16
      • 2020-11-02
      • 1970-01-01
      • 2016-04-29
      相关资源
      最近更新 更多