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

时间:2019-01-30 16:21:51

标签: javascript json reactjs

我一直在考虑使用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属性的对象。这样可以避免与可能试图更新setState()属性的另一个isSubmitting发生冲突/覆盖。

问题2

从性能角度来看,复制当前状态以更改其嵌套属性的最佳方法是什么?

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

即使您可以这样做:

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

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

  

状态 是对更改时组件状态的引用   正在应用。它不应该直接突变。相反,变化   应该以根据来自   状态和道具。

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

由于我们正在处理嵌套对象,因此我们需要一种深度复制对象的方法,并假设您不想使用任何第三方库(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
            }
          }
        });
      });
    }
  );
}

4 个答案:

答案 0 :(得分:3)

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

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

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

答案 1 :(得分:1)

您可以尝试使用嵌套的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
   }
}

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

答案 2 :(得分:1)

我制作了一个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 :(得分:-2)

使用点差运算符

  let {foo} = this.state;

  foo = {
    ...foo,
    bar: baz
  }

  this.setState({
    foo
  })