【问题标题】:What's the best alternative to update nested React state property with setState()?用 setState() 更新嵌套的 React 状态属性的最佳选择是什么?
【发布时间】:2019-06-23 23:55:12
【问题描述】:

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

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

CodeSandbox - Component vs PureComponent and nested state changes

回到这个问题 - 我关心的是在更新状态的嵌套属性时其他并发 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(),以避免与其他可能与此一起排队的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() 以更新@987654347 @。

由于我们正在处理嵌套对象,我们需要一种方法来深度复制对象,并且假设您不想使用任何 3rd 方库 (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() 完成。

第二个选项变得更好performance results。至少好 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
            }
          }
        });
      });
    }
  );
}

【问题讨论】:

标签: javascript json reactjs


【解决方案1】:

我可以想到其他几种方法来实现它。

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

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

【讨论】:

  • 你将如何使用动态键进行解构? inputs: { ...prevState.inputs, [inputName]: { ...prevState.inputs[inputName], value: newValue } }
  • 当我到达...prevState.inputs[inputName]时遇到错误
  • 错误是什么?您确定 inputName 是有效的属性名称吗?另外,投反对票的人(这里的每个答案)可以解释什么
  • 我只是设法让它在 return 语句中明确指定它工作。我将把它添加到我的问题中。您的解决方案比我的更具可读性和明确性。我会在我的表格中采用它。谢谢!
【解决方案2】:

我创建了一个使用动态键更新嵌套状态的 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 并更新以前的状态。

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

【讨论】:

    【解决方案3】:

    你可以试试嵌套Object.Assign:

     const newState = Object.assign({}, state, {
      inputs: Object.assign({}, state.inputs, {
        username: Object.assign({}, state.inputs.username, { touched: true }),
      }),
    });
    

    };

    你也可以使用扩展运算符:

    {
      ...state,
      inputs: {
        ...state.inputs,
          username: {
          ...state.inputs.username,
          touched: true
       }
    }
    

    这是更新嵌套属性并保持状态不可变的正确方法。

    【讨论】:

    • 这对我也有帮助!
    • @cbdev420 如果是这样,您可以将我的答案标记为正确,干杯:)
    【解决方案4】:

    使用扩展运算符

      let {foo} = this.state;
    
      foo = {
        ...foo,
        bar: baz
      }
    
      this.setState({
        foo
      })
    

    【讨论】:

      猜你喜欢
      • 2020-01-16
      • 2014-07-11
      • 2017-08-19
      • 2019-09-12
      • 2021-07-20
      • 2019-12-15
      • 2023-01-31
      • 1970-01-01
      • 2020-12-04
      相关资源
      最近更新 更多