使用 setState() 更新嵌套 React 状态属性的最佳替代方法是什么?

新手上路,请多包涵

我一直在考虑在这些选项中使用 React setState() 方法更新嵌套属性的最佳方式是什么。我也对考虑性能并避免与其他可能的并发状态更改可能发生的冲突的更有效方法持开放态度。

注意: 我正在使用扩展 React.Component 的类组件。如果您正在使用 React.PureComponent 更新嵌套属性时必须格外小心,因为如果您不更改 state 的任何顶级属性,这可能不会触发重新渲染.这是一个说明此问题的沙箱:

CodeSandbox - Component vs PureComponent 和嵌套状态变化

回到这个问题——我关心的是性能和其他并发 setState() 更新状态嵌套属性时调用之间可能发生的冲突:

例子:

假设我正在构建一个表单组件,我将使用以下对象初始化我的表单状态:

 this.state = {
  isSubmitting: false,
  inputs: {
    username: {
      touched: false,
      dirty: false,
      valid: false,
      invalid: false,
      value: 'some_initial_value'
    },
    email: {
      touched: false,
      dirty: false,
      valid: false,
      invalid: false,
      value: 'some_initial_value'
    }
  }
}

根据我的研究,通过使用 setState() ,React 将 浅合并 我们传递给它的对象,这意味着它只会检查顶级属性,在这个例子中是 isSubmittinginputs

因此,我们可以向它传递一个包含这两个顶级属性( isSubmittinginputs )的完整 newState 对象,或者我们可以传递其中一个属性,并将其浅合并到以前的状态。

问题 1

您是否同意仅传递我们正在更新的 state 顶级属性是最佳做法?例如,如果我们不更新 isSubmitting 属性,我们应该避免将它传递给 setState() 在 other 以避免与其他并发调用 setState() 可能发生的冲突/覆盖 --- 那个可能和这个一起排队?这个对吗?

在这个例子中,我们将传递一个只有 inputs 属性的对象。这将避免与另一个可能试图更新 isSubmitting 属性的 setState() 发生冲突/覆盖。

问题2

复制当前状态以更改其嵌套属性的最佳性能方式是什么?

在这种情况下,假设我想设置 state.inputs.username.touched = true

即使你可以这样做:

 this.setState( (state) => {
  state.inputs.username.touched = true;
  return state;
});

你不应该。因为,从 React Docs 中,我们有:

state 是应用更改时对组件状态的引用。它不应该被直接突变。相反,更改应该通过 基于状态和道具的输入构建一个新对象来表示。

因此,从上面的摘录中我们可以推断出我们应该从当前的 state 对象构建一个新对象,以便根据需要更改和操作它并将它传递给 setState() 更新 state

由于我们正在处理嵌套对象,我们需要一种方法来深度复制对象,并且假设您不想使用任何第 3 方库 (lodash) 来这样做,我想出的是:

 this.setState( (state) => {
  let newState = JSON.parse(JSON.stringify(state));
  newState.inputs.username.touched = true;
  return ({
    inputs: newState.inputs
  });
});

请注意,当您的 state 具有嵌套对象时,您也不应该使用 let newState = Object.assign({},state) 。因为那会浅拷贝 state 嵌套对象引用,因此你仍然会直接改变状态,因为 newState.inputs === state.inputs === this.state.inputs 是真的。它们都指向同一个对象 inputs

但是由于 JSON.parse(JSON.stringify(obj)) 有其性能限制,并且还有一些数据类型或循环数据可能对 JSON 不友好,您会推荐什么其他方法来深度复制嵌套对象以更新它?

我想出的另一个解决方案如下:

 this.setState( (state) => {
  let usernameInput = {};
  usernameInput['username'] = Object.assign({},state.inputs.username);
  usernameInput.username.touched = true;
  let newInputs = Object.assign({},state.inputs,usernameInput);

  return({
    inputs: newInputs
  });
};

我在第二种选择中所做的是从我要更新的最里面的对象创建一个新对象(在本例中是 username 对象)。我必须在键中获取这些值 username ,这就是我使用 usernameInput['username'] 的原因,因为稍后我会将其合并到 newInputs 一切都是使用 Object.assign() 完成的。

这第二个选项获得了更好的 性能结果。至少好 50%。

关于这个问题还有其他想法吗?很抱歉问了这么长的问题,但我认为它很好地说明了问题。

编辑:我从以下答案中采用的解决方案:

我的 TextInput 组件 onChange 事件侦听器(我通过 React Context 提供服务):

 onChange={this.context.onChange(this.props.name)}

我的表单组件中的 onChange 函数

onChange(inputName) {
  return(

    (event) => {
      event.preventDefault();
      const newValue = event.target.value;

      this.setState( (prevState) => {

        return({
          inputs: {
            ...prevState.inputs,
            [inputName]: {
              ...prevState.inputs[inputName],
              value: newValue
            }
          }
        });
      });
    }
  );
}

原文由 cbdeveloper 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 833
2 个回答

我可以想到其他一些方法来实现它。

解构每个嵌套元素并只覆盖正确的元素:

 this.setState(prevState => ({
    inputs: {
        ...prevState.inputs,
        username: {
            ...prevState.inputs.username,
            touched: true
        }
    }
}))

使用解构运算符复制您的输入:

 this.setState(prevState => {
    const inputs = {...prevState.inputs};
    inputs.username.touched = true;
    return { inputs }
})


编辑

使用计算属性的第一个解决方案:

 this.setState(prevState => ({
    inputs: {
        ...prevState.inputs,
        [field]: {
            ...prevState.inputs.[field],
            [action]: value
        }
    }
}))

原文由 Treycos 发布,翻译遵循 CC BY-SA 4.0 许可协议

我制作了一个 util 函数,它使用动态键更新嵌套状态。

  function _recUpdateState(state, selector, newval) {
          if (selector.length > 1) {
            let field = selector.shift();
            let subObject = {};
            try {
              //Select the subobject if it exists
              subObject = { ..._recUpdateState(state[field], selector, newval) };
            } catch {
              //Create the subobject if it doesn't exist
              subObject = {
                ..._recUpdateState(state, selector, newval)
              };
            }
            return { ...state, [field]: subObject };
          } else {
            let updatedState = {};
            updatedState[selector.shift()] = newval;
            return { ...state, ...updatedState };
          }
        }

        function updateState(state, selector, newval, autoAssign = true) {
          let newState = _recUpdateState(state, selector, newval);
          if (autoAssign) return Object.assign(state, newState);
          return newState;
        }

// Example

let initState = {
       sub1: {
          val1: "val1",
          val2: "val2",
          sub2: {
             other: "other value",
             testVal: null
          }
       }
    }

    console.log(initState)
    updateState(initState, ["sub1", "sub2", "testVal"], "UPDATED_VALUE")
    console.log(initState)

您传递状态以及键选择器列表和新值。

您还可以将 autoAssign 值设置为 false 以返回一个对象,该对象是旧状态的副本但具有新的更新字段 - 否则 autoAssign = true 并更新以前的状态。

最后,如果选择器序列没有出现在对象中,将创建一个对象和所有具有这些键的嵌套对象。

原文由 vahid 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题