如何更新React中的嵌套状态属性

Ale*_*ong 288 javascript setstate ecmascript-6 reactjs

我试图通过使用这样的嵌套属性来组织我的状态:

this.state = {
   someProperty: {
      flag:true
   }
}
Run Code Online (Sandbox Code Playgroud)

但是像这样更新状态,

this.setState({ someProperty.flag: false });
Run Code Online (Sandbox Code Playgroud)

不起作用.怎么能正确完成?

Shu*_*tri 352

为了setState嵌套对象,您可以遵循以下方法,因为我认为setState不处理嵌套更新.

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})
Run Code Online (Sandbox Code Playgroud)

我们的想法是创建一个虚拟对象对其执行操作,然后用更新的对象替换组件的状态

现在,spread运算符只创建对象的一个​​级别嵌套副本.如果您的州高度嵌套,如:

this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}
Run Code Online (Sandbox Code Playgroud)

您可以在每个级别使用spread运算符setState

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))
Run Code Online (Sandbox Code Playgroud)

但是,随着状态变得越来越嵌套,上面的语法变得越来越难看,因此我建议你使用immutability-helperpackage来更新状态.

请参阅此答案,了解如何使用更新状态immutability helper.

  • 这不违反https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous吗?如果对嵌套对象的两个更改进行了批处理,则最后一个更改将覆盖第一个更改.所以最正确的方法是:`this.setState((prevState)=>({nested:{... prevState.nested,propertyToSet:newValue}})`虽然有点乏味...... (13认同)
  • @Stophface我们可以使用Lodash进行深度克隆,但不是每个人都只将这个库包含在setState中 (5认同)
  • 这对我不起作用..我正在使用反应版@ 15.6.1 (3认同)
  • @Ohgodwhy,不,这不是直接访问状态,因为{... this.state.someProperty}将一个新的obejct返回到`someProperty`,我们正在修改它 (2认同)
  • 我认为你不需要``prevState,`在上一个例子中,你只需要`someProperty`及其子代 (2认同)

Yos*_*eph 120

把它写在一行

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });
Run Code Online (Sandbox Code Playgroud)

  • 建议将updater用于setState,而不是直接访问setState中的this.state.https://reactjs.org/docs/react-component.html#setstate (11认同)
  • 我相信这就是说,不要在`setState`之后立即读取`this.state`并期望它具有新值.在`setState`中调用`this.state`.或者后者还有一个问题我不清楚? (6认同)
  • 正如[trpt4him](/sf/users/61402281/)所说,你给的链接讲了在`setState`之后访问`this.state`的问题。此外,我们没有将 `this.state` 传递给 `setState`。`...` 运算符传播属性。它与 Object.assign() 相同。https://github.com/tc39/proposal-object-rest-spread (3认同)
  • @RaghuTeja也许只是制作`this.setState(currentState => {someProperty:{... currentState.someProperty,flag:false}});`可以避免这个问题吗? (2认同)

Kon*_*nin 70

有时直接答案不是最好的:)

精简版:

这段代码

this.state = {
    someProperty: {
        flag: true
    }
}
Run Code Online (Sandbox Code Playgroud)

应简化为类似的东西

this.state = {
    somePropertyFlag: true
}
Run Code Online (Sandbox Code Playgroud)

长版:

目前,您不应该在React中使用嵌套状态.因为React不适用于嵌套状态,所以这里提出的所有解决方案都看起来像黑客.他们不使用框架,而是与之斗争.他们建议编写不那么清晰的代码,以便对某些属性进行分组.所以他们作为挑战的答案非常有趣但实际上没用.

让我们想象以下状态:

{
    parent: {
        child1: 'value 1',
        child2: 'value 2',
        ...
        child100: 'value 100'
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你只改变一个值会发生什么child1?React不会重新渲染视图,因为它使用浅层比较,它会发现该parent属性没有改变.BTW直接改变状态对象被认为是一种不好的做法.

所以你需要重新创建整个parent对象.但在这种情况下,我们将遇到另一个问题.React会认为所有的孩子都改变了他们的价值观,并且会重新渲染他们所有的孩子.当然这对性能不利.

通过编写一些复杂的逻辑仍然可以解决这个问题,shouldComponentUpdate()但我宁愿停在这里并使用简短版本的简单解决方案.

  • 似乎React并不是为了表示任何存储为非平凡深度的递归结构的东西.这有点令人失望,因为树视图出现在几乎所有复杂的应用程序中(gmail,文件管理器,任何文档管理系统......).我已经使用React来做这些事情,但总是有一个React狂热者告诉大家你正在做反模式,并建议解决方案是在每个节点的状态下保留树的深层副本(是的,80份).我很想看到一个不破坏任何React规则的非hacky树视图组件. (5认同)
  • 我想为你创造一座祭坛,并每天早上为它祷告.我花了三天时间才达到这个答案,完美地解释了OBVIOUS的设计决定.每个人都只是试图使用传播操作符或做其他黑客只是因为嵌套状态看起来更易于阅读. (3认同)
  • 那么,您如何建议使用 React 来做诸如渲染交互式树之类的事情(例如,我的公司在世界不同地区有一堆传感器,每个传感器类型不同,属性不同,位于不同位置(80+ ),当然,它们是按层次结构组织的,因为每次部署都要花费数千美元并且是一个完整的项目,因此如果没有层次结构就不可能管理它们)。我很好奇你如何不在 React 中将树之类的东西建模为......树。 (3认同)
  • 这,一百万次。我花了很多时间搞乱嵌套参数,因为网上无用的评论表明它可能以某种令人困惑的方式出现。React 文档确实应该在 setState 部分的开头指出这一点! (3认同)
  • 我认为在某些情况下,嵌套状态会很好。例如,动态跟踪键值对的状态。此处查看如何仅针对状态中的单个条目更新状态:https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data (2认同)
  • @DanyAlejandro 好的,也许你是 0.1% 需要在 React 状态下使用树的人之一。我没有说严格禁止这样做,我只是说您的应用程序可能因此变得缓慢和/或复杂。如果您可以使用平面状态,则应该这样做。 (2认同)

Qwe*_*rty 51

放弃

React中的嵌套状态是错误的设计

阅读这个优秀的答案.

 

这个答案背后的推理:

React的setState只是一个内置的便利,但你很快意识到它有其局限性.使用自定义属性和智能使用forceUpdate可以提供更多功能.例如:

class MyClass extends React.Component {
    myState = someObject
    inputValue = 42
...
Run Code Online (Sandbox Code Playgroud)

例如,MobX完全抛弃状态并使用自定义可观察属性.
在React组件中使用Observable而不是state.

 


最好的答案 - 见这里的例子

还有另一种更新嵌套属性的更短方法.

this.setState(state => {
  state.nested.flag = false
  state.another.deep.prop = true
  return state
})
Run Code Online (Sandbox Code Playgroud)

在一条线上

this.setState(state => (state.nested.flag = false, state))
Run Code Online (Sandbox Code Playgroud)

当然这是滥用一些核心原则,因为它forceUpdate应该是只读的,但由于你立即丢弃旧状态并用新状态替换它,这是完全没问题的.

警告

即使包含状态的组件正确更新和重新呈现(除了这个问题),道具将无法传播给孩子(请参阅下面的Spymaster评论).如果你知道自己在做什么,只能使用这种技术.

例如,您可以传递更改并轻松传递的更改的平面道具.

this.state.nested.flag = false
this.forceUpdate()
Run Code Online (Sandbox Code Playgroud)

现在即使complexNestedProp的引用没有改变(shouldComponentUpdate)

render(
  //some complex render with your nested state
  <ChildComponent complexNestedProp={this.state.nested} pleaseRerender={Math.random()}/>
)
Run Code Online (Sandbox Code Playgroud)

只要父组件更新,组件就会重新出现,这是在调用之后setStatestate在父组件中更新的情况.

  • 你不应该在任何地方改变任何反应状态.如果有嵌套组件使用state.nested或state.another中的数据,则它们不会重新呈现,也不会重新呈现它们的子项.这是因为对象没有改变,因此React认为没有任何改变. (11认同)
  • @tobiv最好将获取时的数据转换为不同的结构,但这取决于用例。如果您将嵌套对象或对象数组视为_some_数据的紧凑单元,则可以将它们置于状态中。当您需要存储UI开关或其他可观察的数据(即应立即更改视图的数据)时,就会出现问题,因为它们必须是平坦的才能正确触发内部React差异算法。对于其他嵌套对象的更改,很容易触发“ this.forceUpdate()”或实现特定的“ shouldComponentUpdate”。 (2认同)
  • 有点偏离 - 你能提供一些信息为什么 `this.setState(state =&gt; (state.nested.flag = false, state))` 中的 (expression1, expression2) 会这样工作吗?(文档、MDN、搜索请求等) (2认同)
  • 因此,在状态之外拥有整个结构可能会更好,但使用状态链接到您需要在特定时间访问的结构的特定区域。 (2认同)

Aly*_*ose 21

如果您使用的是ES2015,则可以访问Object.assign.您可以按如下方式使用它来更新嵌套对象.

this.setState({
  someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});
Run Code Online (Sandbox Code Playgroud)

您将更新的属性与现有属性合并,并使用返回的对象更新状态.

编辑:向assign函数添加一个空对象作为目标,以确保状态不会像carkod指出的那样直接变异.

  • 我可能错了,但我认为你没有使用React _without_ ES2015. (12认同)

tok*_*and 13

有很多图书馆可以帮助解决这个问题.例如,使用immutability-helper:

import update from 'immutability-helper';

const newState = update(this.state, {
  someProperty: {flag: {$set: false}},
};
this.setState(newState);
Run Code Online (Sandbox Code Playgroud)

或者使用lodash/fp:

import {set} from 'lodash/fp';

const newState = set(["someProperty", "flag"], false, this.state);
Run Code Online (Sandbox Code Playgroud)


enz*_*ito 12

你也可以这样(这对我来说更具可读性):

const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);
Run Code Online (Sandbox Code Playgroud)

  • 这里似乎是更优雅的解决方案,1)避免在 setState 内进行繁琐的传播,2)将新状态很好地放入 setState 内,避免直接改变 setState 内部。最后但并非最不重要的 3)避免仅使用任何库来完成简单的任务 (2认同)

小智 6

这是该线程中给出的第一个答案的变体,它不需要任何额外的包,库或特殊功能.

state = {
  someProperty: {
    flag: 'string'
  }
}

handleChange = (value) => {
  const newState = {...this.state.someProperty, flag: value}
  this.setState({ someProperty: newState })
}
Run Code Online (Sandbox Code Playgroud)

为了设置特定嵌套字段的状态,您已设置整个对象.我这样做是通过创建一个变量,newState然后首先使用ES2015 扩展运算符将当前状态的内容传播到其中.然后,我用this.state.flag新值替换了值(因为我flag: value 将当前状态扩展到对象设置,所以当前状态中的flag字段被覆盖).然后,我只是将状态设置someProperty为我的newState对象.


Joa*_*erg 6

我们使用Immer https://github.com/mweststrate/immer来处理这些问题.

刚刚在我们的一个组件中替换了这段代码

this.setState(prevState => ({
   ...prevState,
        preferences: {
            ...prevState.preferences,
            [key]: newValue
        }
}));
Run Code Online (Sandbox Code Playgroud)

有了这个

import produce from 'immer';

this.setState(produce(draft => {
    draft.preferences[key] = newValue;
}));
Run Code Online (Sandbox Code Playgroud)

使用immer,您可以将状态视为"普通对象".魔术发生在具有代理对象的场景后面.


And*_*rew 6

虽然你问的是基于类的 React 组件的状态,但 useState hook 也存在同样的问题。更糟糕的是:useState 钩子不接受部分更新。因此,当引入 useState 钩子时,这个问题变得非常相关。

我决定发布以下答案,以确保问题涵盖使用 useState 钩子的更现代场景:

如果你有:

const [state, setState] = useState({ someProperty: { flag: true, otherNestedProp: 1 }, otherProp: 2 })
Run Code Online (Sandbox Code Playgroud)

您可以通过克隆当前数据并修补所需的数据段来设置嵌套属性,例如:

setState(current => { ...current, someProperty: { ...current.someProperty, flag: false } });
Run Code Online (Sandbox Code Playgroud)

或者您可以使用 Immer 库来简化对象的克隆和修补。

或者你可以使用Hookstate 库(免责声明:我是作者)来完全简单地管理复杂(本地和全局)状态数据并提高性能(阅读:不用担心渲染优化):

import { useStateLink } from '@hookstate/core' 
const state = useStateLink({ someProperty: { flag: true, otherNestedProp: 1 }, otherProp: 2 })
Run Code Online (Sandbox Code Playgroud)

获取要渲染的字段:

state.nested.someProperty.nested.flag.get()
// or 
state.get().someProperty.flag
Run Code Online (Sandbox Code Playgroud)

设置嵌套字段:

state.nested.someProperty.nested.flag.set(false)
Run Code Online (Sandbox Code Playgroud)

这是 Hookstate 示例,其中状态深度/递归嵌套在树状数据结构中


Alb*_*ras 5

我使用了这个解决方案。

如果您有这样的嵌套状态:

   this.state = {
          formInputs:{
            friendName:{
              value:'',
              isValid:false,
              errorMsg:''
            },
            friendEmail:{
              value:'',
              isValid:false,
              errorMsg:''
            }
}
Run Code Online (Sandbox Code Playgroud)

您可以声明 handleChange 函数来复制当前状态并使用更改的值重新分配它

handleChange(el) {
    let inputName = el.target.name;
    let inputValue = el.target.value;

    let statusCopy = Object.assign({}, this.state);
    statusCopy.formInputs[inputName].value = inputValue;

    this.setState(statusCopy);
  }
Run Code Online (Sandbox Code Playgroud)

这里是带有事件侦听器的 html

<input type="text" onChange={this.handleChange} " name="friendName" />
Run Code Online (Sandbox Code Playgroud)


小智 5

创建状态的副本:

let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))
Run Code Online (Sandbox Code Playgroud)

对此对象进行更改:

someProperty.flag = "false"
Run Code Online (Sandbox Code Playgroud)

现在更新状态

this.setState({someProperty})
Run Code Online (Sandbox Code Playgroud)


小智 5

尽管嵌套并不是您应该如何对待组件状态的真正方法,但有时对于单层嵌套来说还是容易的。

对于这样的状态

state = {
 contact: {
  phone: '888-888-8888',
  email: 'test@test.com'
 }
 address: {
  street:''
 },
 occupation: {
 }
}
Run Code Online (Sandbox Code Playgroud)

我所使用的可重用方法如下所示。

handleChange = (obj) => e => {
  let x = this.state[obj];
  x[e.target.name] = e.target.value;
  this.setState({ [obj]: x });
};
Run Code Online (Sandbox Code Playgroud)

然后只需为您要寻址的每个嵌套传递obj名称...

<TextField
 name="street"
 onChange={handleChange('address')}
 />
Run Code Online (Sandbox Code Playgroud)