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

Posted

技术标签:

【中文标题】React:使用 Hooks 为深度嵌套对象设置状态【英文标题】:React: Setting State for Deeply Nested Objects w/ 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 的值匹配的选定选项(在状态更改之后)。任何帮助将不胜感激。

【问题讨论】:

创建一个可重现的最小示例,以便我们测试问题。 ***.com/help/minimal-reproducible-example 【参考方案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 用于主要代码库(和抛光组件),以避免不必要的重新渲染。幼稚的不完整状态更新会在使用它们时给您带来麻烦,并且可能在其他时候也是如此。

【讨论】:

以上是关于React:使用 Hooks 为深度嵌套对象设置状态的主要内容,如果未能解决你的问题,请参考以下文章

React Hooks - 将状态设置为初始状态

React Hooks源码深度解析

错误:使用 react-hooks 时超出最大更新深度

函数式编程看React Hooks简单React Hooks实现

React hooks:如何观察 JS 类对象的变化?

React:迭代深度嵌套的对象并显示它们而不会撕裂我的头发