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

How to update nested state properties in React

我正在尝试使用嵌套 属性 来组织我的状态,如下所示:

this.state = {
   someProperty: {
      flag:true
   }
}

但是像这样更新状态,

this.setState({ someProperty.flag: false });

不起作用。如何正确完成?

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

var someProperty = {...this.state.someProperty}
someProperty.flag = true;
this.setState({someProperty})

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

现在,展开运算符只创建对象的一层嵌套副本。如果您的状态高度嵌套,例如:

this.state = {
   someProperty: {
      someOtherProperty: {
          anotherProperty: {
             flag: true
          }
          ..
      }
      ...
   }
   ...
}

您可以在每个级别使用扩展运算符来设置状态,例如

this.setState(prevState => ({
    ...prevState,
    someProperty: {
        ...prevState.someProperty,
        someOtherProperty: {
            ...prevState.someProperty.someOtherProperty, 
            anotherProperty: {
               ...prevState.someProperty.someOtherProperty.anotherProperty,
               flag: false
            }
        }
    }
}))

然而,随着状态变得越来越嵌套,上述语法变得越来越丑陋,因此我建议您使用 immutability-helper 包来更新状态。

请参阅 了解如何使用 immutability-helper 更新状态。

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

import update from 'immutability-helper';

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

使用lodash/fp设置:

import {set} from 'lodash/fp';

const newState = set(["someProperty", "flag"], false, this.state);

使用lodash/fp合并:

import {merge} from 'lodash/fp';

const newState = merge(this.state, {
  someProperty: {flag: false},
});

写成一行

this.setState({ someProperty: { ...this.state.someProperty, flag: false} });

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

this.setState({
  someProperty: Object.assign({}, this.state.someProperty, {flag: false})
});

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

编辑:添加一个空对象作为分配函数的目标,以确保状态不会像 carkod 指出的那样直接发生变化。

这是此线程中给出的第一个答案的变体,它不需要任何额外的包、库或特殊函数。

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

handleChange = (value) => {
  const newState = {...this.state.someProperty, flag: value}
  this.setState({ someProperty: newState })
}

为了设置特定嵌套字段的状态,您设置了整个对象。我通过创建一个变量 newState 并将当前状态的内容传播到其中 first 使用 ES2015 spread operator 来做到这一点。然后,我用新值替换了 this.state.flag 的值(因为我设置 flag: value 之后我将当前状态传播到对象中, flag 字段在当前状态被覆盖)。然后,我简单地将 someProperty 的状态设置为我的 newState 对象。

我使用了这个解决方案。

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

   this.state = {
          formInputs:{
            friendName:{
              value:'',
              isValid:false,
              errorMsg:''
            },
            friendEmail:{
              value:'',
              isValid:false,
              errorMsg:''
            }
}

您可以声明复制当前状态的 handleChange 函数,并re-assigns它具有更改后的值

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

此处 html 与事件侦听器

<input type="text" onChange={this.handleChange} " name="friendName" />
const newState = Object.assign({}, this.state);
newState.property.nestedProperty = "new value";
this.setState(newState);

免责声明

Nested State in React is wrong design

Read .

Reasoning behind this answer:

React's setState is just a built-in convenience, but you soon realise that it has its limits. Using custom properties and intelligent use of forceUpdate gives you much more. eg:

class MyClass extends React.Component {
    myState = someObject
    inputValue = 42
...

MobX, for example, ditches state completely and uses custom observable properties.
Use Observables instead of state in React components.


你痛苦的答案 - see example here

还有另一种更短的方法来更新任何嵌套的属性。

this.setState(state => {
  state.nested.flag = false
  state.another.deep.prop = true
  return state
})

在一条线上

 this.setState(state => (state.nested.flag = false, state))

注意:这里是Comma operator ~MDN, see it in action here (Sandbox).

它类似于(虽然这不会改变状态引用)

this.state.nested.flag = false
this.forceUpdate()

有关 forceUpdatesetState 之间的细微差别,请参阅链接的 example and sandbox

当然这是在滥用一些核心原则,因为 state 应该是 read-only,但是由于您立即丢弃旧状态并用新状态替换它,所以完全可以。

警告

即使包含状态 的组件将 正确更新和重新渲染 (),道具将 无法 传播到 children (请参阅下面 Spymaster 的评论)。仅当您知道自己在做什么时才使用此技术。

例如,您可能会传递一个更改的平面道具,该道具很容易更新和传递。

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

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

this.props.complexNestedProp === nextProps.complexNestedProp

组件 将在 parent 组件更新时 重新呈现,这是在 [=107] 中调用 this.setStatethis.forceUpdate 后的情况=].

改变状态的效果sandbox

使用 嵌套状态 并直接改变状态是危险的,因为不同的 objects 可能(有意或无意)持有对 [=83= 的不同(旧)引用]state 并且可能不一定知道何时更新(例如当使用 PureComponent 或如果 shouldComponentUpdate 实现为 return falseOR 旨在显示旧数据,如下例所示。

Imagine a timeline that is supposed to render historic data, mutating the data under the hand will result in unexpected behaviour as it will also change previous items.

无论如何你可以看到 Nested PureChildClass 由于 props 传播失败而没有重新渲染。

我发现这对我有用,在我的例子中有一个项目表单,例如你有一个 ID 和一个名称,我宁愿维护嵌套项目的状态。

return (
  <div>
      <h2>Project Details</h2>
      <form>
        <Input label="ID" group type="number" value={this.state.project.id} onChange={(event) => this.setState({ project: {...this.state.project, id: event.target.value}})} />
        <Input label="Name" group type="text" value={this.state.project.name} onChange={(event) => this.setState({ project: {...this.state.project, name: event.target.value}})} />
      </form> 
  </div>
)

告诉我!

另外两个未提及的选项:

  1. 如果您的状态嵌套很深,请考虑是否可以重组子对象以使其位于根部。这使得数据更容易更新。
  2. 有许多方便的库可用于处理不可变状态 listed in the Redux docs。我推荐 Immer,因为它允许您以可变方式编写代码,但会在幕后处理必要的克隆。它还会冻结生成的对象,这样您以后就不会不小心改变它。

为了让事情变得通用,我研究了@ShubhamKhatri 和@Qwerty 的答案。

状态对象

this.state = {
  name: '',
  grandParent: {
    parent1: {
      child: ''
    },
    parent2: {
      child: ''
    }
  }
};

输入控件

<input
  value={this.state.name}
  onChange={this.updateState}
  type="text"
  name="name"
/>
<input
  value={this.state.grandParent.parent1.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent1.child"
/>
<input
  value={this.state.grandParent.parent2.child}
  onChange={this.updateState}
  type="text"
  name="grandParent.parent2.child"
/>

updateState 方法

setState 作为@ShubhamKhatri 的回答

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const oldstate = this.state;
  const newstate = { ...oldstate };
  let newStateLevel = newstate;
  let oldStateLevel = oldstate;

  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      newStateLevel[path[i]] = event.target.value;
    } else {
      newStateLevel[path[i]] = { ...oldStateLevel[path[i]] };
      oldStateLevel = oldStateLevel[path[i]];
      newStateLevel = newStateLevel[path[i]];
    }
  }
  this.setState(newstate);
}

将状态设置为@Qwerty 的回答

updateState(event) {
  const path = event.target.name.split('.');
  const depth = path.length;
  const state = { ...this.state };
  let ref = state;
  for (let i = 0; i < depth; i += 1) {
    if (i === depth - 1) {
      ref[path[i]] = event.target.value;
    } else {
      ref = ref[path[i]];
    }
  }
  this.setState(state);
}

注意:以上方法不适用于数组

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

短版:

此代码

this.state = {
    someProperty: {
        flag: true
    }
}

应该简化为

this.state = {
    somePropertyFlag: true
}

长版:

目前你不应该想在 React 中使用嵌套状态。因为 React 不适合处理嵌套状态,所以这里提出的所有解决方案看起来都是 hack。他们不使用框架,而是与之抗争。他们建议编写不太清晰的代码,以用于对某些属性进行分组的可疑目的。因此,它们作为挑战的答案非常有趣,但实际上毫无用处。

让我们想象以下状态:

{
    parent: {
        child1: 'value 1',
        child2: 'value 2',
        ...
        child100: 'value 100'
    }
}

如果只更改 child1 的值会发生什么? React 不会 re-render 视图,因为它使用浅层比较,它会发现 parent 属性 没有改变。顺便说一句,直接改变状态 object 通常被认为是一种不好的做法。

所以你需要re-create整个parentobject。但是在这种情况下我们会遇到另一个问题。 React 会认为所有 children 都改变了它们的值,并且会 re-render 所有这些。当然对性能不好。

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

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

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

this.setState(prevState => ({
   ...prevState,
        preferences: {
            ...prevState.preferences,
            [key]: newValue
        }
}));

有了这个

import produce from 'immer';

this.setState(produce(draft => {
    draft.preferences[key] = newValue;
}));

使用 immer,您可以像 "normal object" 一样处理您的状态。 魔术发生在代理对象的幕后。

像这样可能就足够了,

const isObject = (thing) => {
    if(thing && 
        typeof thing === 'object' &&
        typeof thing !== null
        && !(Array.isArray(thing))
    ){
        return true;
    }
    return false;
}

/*
  Call with an array containing the path to the property you want to access
  And the current component/redux state.

  For example if we want to update `hello` within the following obj
  const obj = {
     somePrimitive:false,
     someNestedObj:{
        hello:1
     }
  }

  we would do :
  //clone the object
  const cloned = clone(['someNestedObj','hello'],obj)
  //Set the new value
  cloned.someNestedObj.hello = 5;

*/
const clone = (arr, state) => {
    let clonedObj = {...state}
    const originalObj = clonedObj;
    arr.forEach(property => {
        if(!(property in clonedObj)){
            throw new Error('State missing property')
        }

        if(isObject(clonedObj[property])){
            clonedObj[property] = {...originalObj[property]};
            clonedObj = clonedObj[property];
        }
    })
    return originalObj;
}

const nestedObj = {
    someProperty:true,
    someNestedObj:{
        someOtherProperty:true
    }
}

const clonedObj = clone(['someProperty'], nestedObj);
console.log(clonedObj === nestedObj) //returns false
console.log(clonedObj.someProperty === nestedObj.someProperty) //returns true
console.log(clonedObj.someNestedObj === nestedObj.someNestedObj) //returns true

console.log()
const clonedObj2 = clone(['someProperty','someNestedObj','someOtherProperty'], nestedObj);
console.log(clonedObj2 === nestedObj) // returns false
console.log(clonedObj2.someNestedObj === nestedObj.someNestedObj) //returns false
//returns true (doesn't attempt to clone because its primitive type)
console.log(clonedObj2.someNestedObj.someOtherProperty === nestedObj.someNestedObj.someOtherProperty) 

创建状态副本:

let someProperty = JSON.parse(JSON.stringify(this.state.someProperty))

在此对象中进行更改:

someProperty.flag = "false"

现在更新状态

this.setState({someProperty})

我非常重视这些问题 around creating a complete copy of your component state. With that said, I would strongly suggest Immer

import produce from 'immer';

<Input
  value={this.state.form.username}
  onChange={e => produce(this.state, s => { s.form.username = e.target.value }) } />

这应该适用于 React.PureComponent(即 React 的浅状态比较),因为 Immer 巧妙地使用代理对象来有效地复制任意深度的状态树。与 Immutability Helper 等库相比,Immer 的类型也更安全,非常适合 Javascript 和 Typescript 用户。


Typescript效用函数

function setStateDeep<S>(comp: React.Component<any, S, any>, fn: (s: 
Draft<Readonly<S>>) => any) {
  comp.setState(produce(comp.state, s => { fn(s); }))
}

onChange={e => setStateDeep(this, s => s.form.username = e.target.value)}

我知道这是一个老问题,但仍然想分享我是如何做到这一点的。假设构造函数中的状态如下所示:

  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      user: {
        email: ""
      },
      organization: {
        name: ""
      }
    };

    this.handleChange = this.handleChange.bind(this);
  }

我的handleChange函数是这样的:

  handleChange(e) {
    const names = e.target.name.split(".");
    const value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
    this.setState((state) => {
      state[names[0]][names[1]] = value;
      return {[names[0]]: state[names[0]]};
    });
  }

并确保相应地命名输入:

<input
   type="text"
   name="user.email"
   onChange={this.handleChange}
   value={this.state.user.firstName}
   placeholder="Email Address"
/>

<input
   type="text"
   name="organization.name"
   onChange={this.handleChange}
   value={this.state.organization.name}
   placeholder="Organization Name"
/>

虽然嵌套并不是真正应该如何处理组件状态,但有时为了简单的单层嵌套。

对于这样的状态

state = {
 contact: {
  phone: '888-888-8888',
  email: 'test@test.com'
 }
 address: {
  street:''
 },
 occupation: {
 }
}

我使用的 re-useable 方法如下所示。

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

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

<TextField
 name="street"
 onChange={handleChange('address')}
 />
stateUpdate = () => {
    let obj = this.state;
    if(this.props.v12_data.values.email) {
      obj.obj_v12.Customer.EmailAddress = this.props.v12_data.values.email
    }
    this.setState(obj)
}

我使用 reduce 搜索进行嵌套更新:

示例:

状态中的嵌套变量:

state = {
    coords: {
        x: 0,
        y: 0,
        z: 0
    }
}

函数:

handleChange = nestedAttr => event => {
  const { target: { value } } = event;
  const attrs = nestedAttr.split('.');

  let stateVar = this.state[attrs[0]];
  if(attrs.length>1)
    attrs.reduce((a,b,index,arr)=>{
      if(index==arr.length-1)
        a[b] = value;
      else if(a[b]!=null)
        return a[b]
      else
        return a;
    },stateVar);
  else
    stateVar = value;

  this.setState({[attrs[0]]: stateVar})
}

使用:

<input
value={this.state.coords.x}
onChange={this.handleTextChange('coords.x')}
/>

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

我决定post以下答案以确保问题涵盖更多使用 useState 挂钩的现代场景:

如果你有:

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

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

setState(current => { ...current, someProperty: { ...current.someProperty, flag: false } });

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

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

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

获取要呈现的字段:

state.nested.someProperty.nested.flag.get()
// or 
state.get().someProperty.flag

设置嵌套字段:

state.nested.someProperty.nested.flag.set(false)

这是 Hookstate 示例,其中状态深度/递归嵌套在 tree-like data structure

这是我的初始状态

    const initialStateInput = {
        cabeceraFamilia: {
            familia: '',
            direccion: '',
            telefonos: '',
            email: ''
        },
        motivoConsulta: '',
        fechaHora: '',
        corresponsables: [],
    }

钩子或者你可以用状态(class组件)替换它

const [infoAgendamiento, setInfoAgendamiento] = useState(initialStateInput);

handleChange 方法

const actualizarState = e => {
    const nameObjects = e.target.name.split('.');
    const newState = setStateNested(infoAgendamiento, nameObjects, e.target.value);
    setInfoAgendamiento({...newState});
};

使用嵌套状态设置状态的方法

const setStateNested = (state, nameObjects, value) => {
    let i = 0;
    let operativeState = state;
    if(nameObjects.length > 1){
        for (i = 0; i < nameObjects.length - 1; i++) {
            operativeState = operativeState[nameObjects[i]];
        }
    }
    operativeState[nameObjects[i]] = value;
    return state;
}

最后这是我使用的输入

<input type="text" className="form-control" name="cabeceraFamilia.direccion" placeholder="Dirección" defaultValue={infoAgendamiento.cabeceraFamilia.direccion} onChange={actualizarState} />

如果您在项目中使用 formik,它有一些简单的方法来处理这些东西。这是使用 formik 最简单的方法。

首先在 formik initivalues 属性或反应中设置初始值。状态

这里,初始值是在react state中定义的

   state = { 
     data: {
        fy: {
            active: "N"
        }
     }
   }

在 formik initiValues 属性中为 formik 字段定义上面的初始值

<Formik
 initialValues={this.state.data}
 onSubmit={(values, actions)=> {...your actions goes here}}
>
{({ isSubmitting }) => (
  <Form>
    <Field type="checkbox" name="fy.active" onChange={(e) => {
      const value = e.target.checked;
      if(value) setFieldValue('fy.active', 'Y')
      else setFieldValue('fy.active', 'N')
    }}/>
  </Form>
)}
</Formik>

创建一个控制台来检查状态更新到 string 而不是 boolean formik setFieldValue 函数来设置状态或使用 React 调试器工具来查看内部的变化formik 状态值。

试试这个代码:

this.setState({ someProperty: {flag: false} });

这显然不是正确或最好的方法,但我认为它更清晰:

this.state.hugeNestedObject = hugeNestedObject; 
this.state.anotherHugeNestedObject = anotherHugeNestedObject; 

this.setState({})

但是,React 本身应该迭代思想嵌套对象并更新状态,并且 DOM 相应地还没有。

你可以通过对象传播来做到这一点 代码:

 this.setState((state)=>({ someProperty:{...state.someProperty,flag:false}})

这将适用于更多嵌套 属性

将其用于多输入控件和动态嵌套名称

<input type="text" name="title" placeholder="add title" onChange={this.handleInputChange} />
<input type="checkbox" name="chkusein" onChange={this.handleInputChange} />
<textarea name="body" id="" cols="30" rows="10" placeholder="add blog content" onChange={this.handleInputChange}></textarea>

代码可读性强

处理程序

handleInputChange = (event) => {
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name;
        const newState = { ...this.state.someProperty, [name]: value }
        this.setState({ someProperty: newState })
    }

还有另一个选项,如果对象列表中有多个项目,此选项有效:使用 this.state.Obj 将对象复制到变量(比如 temp),使用 filter() 方法遍历对象并将要更改的特定元素放入一个对象(将其命名为 updateObj),将剩余的对象列表放入另一个对象(将其命名为 restObj)。现在编辑要更新的对象的内容,创建一个新项目(比如 newItem)。然后调用 this.setUpdate() 并使用扩展运算符将新的对象列表分配给父对象。

this.state = {someProperty: { flag:true, }}


var temp=[...this.state.someProperty]
var restObj = temp.filter((item) => item.flag !== true);
var updateObj = temp.filter((item) => item.flag === true);

var newItem = {
  flag: false
};
this.setState({ someProperty: [...restObj, newItem] });

不确定根据框架的标准这在技术上是否正确,但有时您只需要更新嵌套对象。这是我使用钩子的解决方案。

setInputState({
                ...inputState,
                [parentKey]: { ...inputState[parentKey], [childKey]: value },
            });

我看到每个人都给出了基于 class 的组件状态更新解决方案,这是预期的,因为他要求这样做,但我正在尝试为 hook 提供相同的解决方案。

const [state, setState] = useState({
    state1: false,
    state2: 'lorem ipsum'
})

现在如果你想改变嵌套对象键state1只有你可以做以下任何一个:

进程 1

let oldState = state;
oldState.state1 = true
setState({...oldState);

进程 2

setState(prevState => ({
    ...prevState,
    state1: true
}))

我最喜欢流程2

如果要动态设置状态


以下示例动态设置表单状态,其中状态中的每个键都是对象

 onChange(e:React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
    this.setState({ [e.target.name]: { ...this.state[e.target.name], value: e.target.value } });
  }

您应该将新状态传递给 setState。 新状态的引用必须与旧状态不同。

所以试试这个:

this.setState({
    ...this.state,
    someProperty: {...this.state.someProperty, flag: true},
})