React:如何在setState上更新state.item [1]? (用JSFiddle)

时间:2015-04-09 11:28:44

标签: javascript reactjs

我正在创建一个用户可以设计自己的表单的应用程序。例如。指定字段的名称以及应包含哪些其他列的详细信息。

该组件可用作JSFiddle here

我的初始状态如下:

var DynamicForm = React.createClass({
  getInitialState: function() {
   var items = {};
   items[1] = { name: 'field 1', populate_at: 'web_start',
                same_as: 'customer_name',
                autocomplete_from: 'customer_name', title: '' };
   items[2] = { name: 'field 2', populate_at: 'web_end',
                same_as: 'user_name', 
                    autocomplete_from: 'user_name', title: '' };

     return { items };
   },

  render: function() {
     var _this = this;
     return (
       <div>
         { Object.keys(this.state.items).map(function (key) {
           var item = _this.state.items[key];
           return (
             <div>
               <PopulateAtCheckboxes this={this}
                 checked={item.populate_at} id={key} 
                   populate_at={data.populate_at} />
            </div>
            );
        }, this)}
        <button onClick={this.newFieldEntry}>Create a new field</button>
        <button onClick={this.saveAndContinue}>Save and Continue</button>
      </div>
    );
  }

我想在用户更改任何值时更新状态,但我很难定位正确的对象:

var PopulateAtCheckboxes = React.createClass({
  handleChange: function (e) {
     item = this.state.items[1];
     item.name = 'newName';
     items[1] = item;
     this.setState({items: items});
  },
  render: function() {
    var populateAtCheckbox = this.props.populate_at.map(function(value) {
      return (
        <label for={value}>
          <input type="radio" name={'populate_at'+this.props.id} value={value}
            onChange={this.handleChange} checked={this.props.checked == value}
            ref="populate-at"/>
          {value}
        </label>
      );
    }, this);
    return (
      <div className="populate-at-checkboxes">
        {populateAtCheckbox}
      </div>
    );
  }
});

我应该如何制作this.setState来更新items[1].name

25 个答案:

答案 0 :(得分:85)

您可以使用update immutability helper for this

this.setState({
  items: update(this.state.items, {1: {name: {$set: 'updated field name'}}})
})

或者如果您不关心能够使用shouldComponentUpdate()===生命周期方法中检测此项目的更改,您可以直接编辑状态并强制组件重新启动渲染 - 这实际上与@ limelights&#39;相同回答,因为它将一个物体拉出状态并进行编辑。

this.state.items[1].name = 'updated field name'
this.forceUpdate()

编辑后添加:

Simple Component Communication查看react-training课程,了解如何将状态保持父级的回调函数传递给需要触发状态更改的子组件。

答案 1 :(得分:79)

错误的方式!

handleChange = (e) => {
    const { items } = this.state;
    items[1].name = e.target.value;

    // update state
    this.setState({
        items,
    });
};

正如评论中许多更好的开发者所指出的:改变状态是错误的!

我花了一些时间来弄明白这一点。以上工作,但它夺走了React的力量。例如,componentDidUpdate不会将此视为更新,因为它已直接修改。

所以正确的方式将是:

handleChange = (e) => {
    this.setState(prevState => ({
        items: {
            ...prevState.items,
            [prevState.items[1].name]: e.target.value,
        },
    }));
};

答案 2 :(得分:64)

由于这个帖子中有很多错误信息,所以如果没有帮助库,你可以做到这一点:

handleChange: function (e) {
    // 1. Make a shallow copy of the items
    let items = [...this.state.items];
    // 2. Make a shallow copy of the item you want to mutate
    let item = {...items[1]};
    // 3. Replace the property you're intested in
    item.name = 'newName';
    // 4. Put it back into our array. N.B. we *are* mutating the array here, but that's why we made a copy first
    items[1] = item;
    // 5. Set the state to our new copy
    this.setState({items});
},

如果需要,您可以结合第2步和第3步:

let item = {
    ...items[1],
    name: 'newName'
}

或者你可以在一行中完成整个事情:

this.setState(({items}) => ({
    items: [
        ...items.slice(0,1),
        {
            ...items[1],
            name: 'newName',
        },
        ...items.slice(2)
    ]
}));

注意:我将items作为一个数组。 OP使用了一个对象。但是,概念是相同的。

您可以在终端/控制台中查看正在进行的操作:

❯ node
> items = [{name:'foo'},{name:'bar'},{name:'baz'}]
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ]
> clone = [...items]
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ]
> item1 = {...clone[1]}
{ name: 'bar' }
> item1.name = 'bacon'
'bacon'
> clone[1] = item1
{ name: 'bacon' }
> clone
[ { name: 'foo' }, { name: 'bacon' }, { name: 'baz' } ]
> items
[ { name: 'foo' }, { name: 'bar' }, { name: 'baz' } ] // good! we didn't mutate `items`
> items === clone
false // these are different objects
> items[0] === clone[0]
true // we don't need to clone items 0 and 2 because we're not mutating them (efficiency gains!)
> items[1] === clone[1]
false // this guy we copied

答案 3 :(得分:41)

要在React的状态下修改深层嵌套的对象/变量,通常使用三种方法:来自immutability-helper的vanilla JS Object.assignLodashcloneDeep 。还有很多其他不太受欢迎的第三方库来实现这一目标,但在这个答案中,我将仅涵盖这三个选项。此外,还有一些方法只使用vanilla JavaScript,比如数组传播(例如,请参阅@ mpen的回答),但它们不是非常直观,易于使用并且能够处理所有状态操作情况。

正如无数次在最高投票评论中指出的答案,其作者提出直接的状态变异,只是不做那个。这是一种无处不在的React反模式,它将不可避免地导致不必要的后果。学习正确的方法。

让我们比较三种广泛使用的方法。

鉴于这种状态结构:

state = {
    foo: {
        bar: 'initial value'
    }
}

1。 Vanilla JavaScript的Object.assign

(...essential imports)
class App extends Component {

    state = {
        foo: {
            bar: 'initial value'
        }
    }

    componentDidMount() {

        console.log(this.state.foo.bar) // initial value

        const foo = Object.assign({}, this.state.foo, { bar: 'further value' })

        console.log(this.state.foo.bar) // initial value

        this.setState({ foo }, () => {
            console.log(this.state.foo.bar) // further value
        })
    }
    (...rest of code)

请注意,Object.assign 不会执行深度克隆,因为it only copies property values,这就是为什么它的作用被称为浅层复制(见评论)。

为此,我们只应操纵此对象的顶级state.foo)。它们的值(state.foo.bar)应为primitive(字符串,数字,布尔值)。

在此示例中,我们使用const foo...创建一个新常量(Object.assign),创建一个空对象({}),复制state.foo将对象({ bar: 'initial value' })放入其中,然后在其上复制另一个对象{ bar: 'further value' }。因此,最终,新创建的foo常量将保留值{ bar: 'further value' },因为bar属性被覆盖。这个foo是一个全新的对象,它没有链接到状态对象,所以它可以根据需要进行变异,状态也不会改变。

最后一部分是使用setState() setter用新创建的state.foo对象替换状态中的原始foo

现在假设我们有一个更深层的状态,如state = { foo: { bar: { baz: 'initial value' } } }。我们可以尝试创建一个新的foo对象,并使用该州的foo内容填充该对象,但Object.assign将无法将baz值复制到此新创建的foo baz对象,因为bar嵌套太深。您仍然可以复制state.foo.bar,就像上面的示例一样,但由于它现在是一个对象而不是原始对象,因此将复制foo的引用,这意味着我们将结束与本地foo对象直接绑定到状态。这意味着在这种情况下,本地创建的state.foo的任何突变都会影响Object.assign对象,因为它们实际上指向同一个东西。

因此,

Object.assign只有在你有一个相对简单的一级深度状态结构时才有效,最内层成员保持基本类型的值。

如果您有更深层次的对象(第二级或更高级别),您应该更新,请不要使用(...essential imports) import cloneDeep from 'lodash.clonedeep' class App extends Component { state = { foo: { bar: 'initial value' } } componentDidMount() { console.log(this.state.foo.bar) // initial value const foo = cloneDeep(this.state.foo) foo.bar = 'further value' console.log(this.state.foo.bar) // initial value this.setState({ foo }, () => { console.log(this.state.foo.bar) // further value }) } (...rest of code) 。你可能会直接改变国家的风险。

2。 Lodash的cloneDeep

cloneDeep()

Lodash的cloneDeep更简单易用。它执行深度克隆,因此如果你有一个内部有多级对象或数组的相当复杂的状态,它是一个强大的选项。只需setState()顶级州财产,随意改变克隆部分,并(...essential imports) import update from 'immutability-helper' class App extends Component { state = { foo: { bar: 'initial value' } }; componentDidMount() { console.log(this.state.foo.bar) // initial value const foo = update(this.state.foo, { bar: { $set: 'further value' } }) console.log(this.state.foo.bar) // initial value this.setState({ foo }, () => { console.log(this.state.foo.bar) // further value }); } (...rest of code) 将其恢复到州。

3。的不变性辅助

immutability-helper

$set将其带到了全新的水平,而关于它的一个很酷的事情是,它不仅可以$push值来表示项目,还可以$splice,{{1} },$merge(等)他们。这是一个list of commands可用。

旁注

同样,请记住,this.setState()仅修改状态对象的第一级属性(在本例中为foo属性),而不是深层嵌套( foo.bar)。如果它以另一种方式表现,那么这个问题就不存在了。

顺便说一下,this.setState({ foo })只是this.setState({ foo: foo })的简写。 () => { console.log(this.state.foo.bar) }之后的{ foo }是一个回调,在setState设置状态后立即执行。方便的话,如果你在完成它的工作后需要做一些事情(在我们的情况下,在设置之后立即显示状态)。

您的项目中哪一个正确

如果您不想或不能使用外部依赖,并且简单的州结构,请坚持Object.assign

如果你操纵一个巨大的和/或复杂的状态,Lodash的cloneDeep是明智的选择。

如果您需要高级功能,即如果您的州结构很复杂且需要对其执行各种操作,请尝试immutability-helper,这是一个非常先进的工具可以用于状态操纵。

答案 4 :(得分:25)

首先获取您想要的项目,在该对象上更改您想要的内容并将其重新设置为状态。 如果您使用键控对象,那么通过仅在getInitialState中传递对象来使用状态的方式会更容易。

handleChange: function (e) {
   item = this.state.items[1];
   item.name = 'newName';
   items[1] = item;

   this.setState({items: items});
}

答案 5 :(得分:22)

不要改变状态。它可能会导致意外的结果。我已经吸取了教训!始终使用副本/克隆,Object.assign()是一个很好的:

item = Object.assign({}, this.state.items[1], {name: 'newName'});
items[1] = item;
this.setState({items: items});

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

答案 6 :(得分:22)

我遇到了同样的问题。这是一个有效的简单解决方案!

const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });

答案 7 :(得分:19)

根据setState上的React文档,按照此处其他答案的建议使用Object.assign是不理想的。由于setState的异步行为的性质,使用此技术的后续调用可能会覆盖先前的调用,从而导致不良后果。

相反,React文档建议使用setState的更新程序形式,该形式在先前的状态下运行。请记住,在更新数组或对象时,您必须返回一个新的数组或对象,因为React要求我们保留状态不变性。使用ES6语法的spread运算符浅表复制数组,在数组的给定索引处创建或更新对象的属性,如下所示:

this.setState(prevState => {
    const newItems = [...prevState.items];
    newItems[index].name = newName;
    return {items: newItems};
})

答案 8 :(得分:3)

这很简单。

首先从状态中拉出整个项目对象,根据需要更新项目对象的一部分,然后通过setState将整个项目对象重新置于状态。

handleChange: function (e) {
  items = Object.assign(this.state.items); // Pull the entire items object out. Using object.assign is a good idea for objects.
  items[1].name = 'newName'; // update the items object as needed
  this.setState({ items }); // Put back in state
}

答案 9 :(得分:2)

在一行中使用带有箭头功能的数组映射

this.setState({
    items: this.state.items.map((item, index) =>
      index === 1 ? { ...item, name: 'newName' } : item,
   )
})

答案 10 :(得分:1)

有时在React中,对克隆的数组进行突变会影响原始数组,这种方法永远不会导致突变:

    const myNewArray = Object.assign([...myArray], {
        [index]: myNewItem
    });
    setState({ myArray: myNewArray });

或者,如果您只想更新商品的属性:

    const myNewArray = Object.assign([...myArray], {
        [index]: {
            ...myArray[index],
            prop: myNewValue
        }
    });
    setState({ myArray: myNewArray });

答案 11 :(得分:1)

发现这一点非常困难,而且ES6传播魔术似乎都没有按预期工作。 正在使用这样的结构来获取渲染的元素属性以用于布局。

使用update中的immutability-helper方法在此简化示例中是最直接的方法:

constructor(props) {
    super(props)
    this.state = { values: [] }
    this.updateContainerState = this.updateContainerState.bind(this)
  }

updateContainerState(index, value) {
    this.setState((state) => update(state, { values: { [index]: { $set: value } } }))
  }

改编自https://github.com/kolodny/immutability-helper#computed-property-names

要更新的数组成员中的

是嵌套更复杂的复杂对象,请根据复杂性使用appropriate deep copy method

肯定有更好的方法来处理布局参数,但这与如何处理数组有关。每个子元素的相关值也可以在它们外部进行计算,但是我发现将containerState向下传递更为方便,因此它们的子元素可以随意获取属性,并在给定的索引处更新父状态数组。

import React from 'react'
import update from 'immutability-helper'
import { ContainerElement } from './container.component.style.js'
import ChildComponent from './child-component'
export default class ContainerComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = { values: [] }
    this.updateContainerState = this.updateContainerState.bind(this)
  }

  updateContainerState(index, value) {
    this.setState((state) => update(state, { values: { [index]: { $set: value } } }))
  }

  // ...

  render() {
    let index = 0
    return (
      <ContainerElement>
      <ChildComponent
        index={index++}
        containerState={this.state}
        updateContainerState={this.updateContainerState}
      />
      <ChildComponent
        index={index++}
        containerState={this.state}
        updateContainerState={this.updateContainerState}
      />
      </ContainerElement>
    )
  }
}

答案 12 :(得分:1)

无变异:

// given a state
state = {items: [{name: 'Fred', value: 1}, {name: 'Wilma', value: 2}]}

// This will work without mutation as it clones the modified item in the map:
this.state.items
   .map(item => item.name === 'Fred' ? {...item, ...{value: 3}} : item)

this.setState(newItems)

答案 13 :(得分:0)

如果您只需要更改Array的一部分, 你有一个反应组件,状态设置为。

state = {items: [{name: 'red-one', value: 100}, {name: 'green-one', value: 999}]}

最好更新red-one中的Array,如下所示:

const itemIndex = this.state.items.findIndex(i=> i.name === 'red-one');
const newItems = [
   this.state.items.slice(0, itemIndex),
   {name: 'red-one', value: 666},
   this.state.items.slice(itemIndex)
]

this.setState(newItems)

答案 14 :(得分:0)

由于上述选项都不适合我,所以我最终使用了map:

this.setState({items: this.state.items.map((item,idx)=> idx!==1 ?item :{...item,name:'new_name'}) })

答案 15 :(得分:0)

,或者如果您有一个动态生成的列表,但您不知道索引,而只有键或ID:

let ItemsCopy = []
let x = this.state.Items.map((entry) =>{

    if(entry.id == 'theIDYoureLookingFor')
    {
        entry.PropertyToChange = 'NewProperty'
    }

    ItemsCopy.push(entry)
})


this.setState({Items:ItemsCopy});

答案 16 :(得分:0)

尝试使用代码:

this.state.items[1] = 'new value';
var cloneObj = Object.assign({}, this.state.items);

this.setState({items: cloneObj });

答案 17 :(得分:0)

在我沉闷的大脑中,遵循下面的代码很容易。删除对象并替换为更新的对象

    var udpateditem = this.state.items.find(function(item) { 
                   return item.name == "field_1" });
    udpateditem.name= "New updated name"                       
    this.setState(prevState => ({                                   
    items:prevState.dl_name_template.filter(function(item) { 
                                    return item.name !== "field_1"}).concat(udpateditem)
    }));

答案 18 :(得分:0)

如何创建另一个组件(对于需要进入数组的对象)并将以下内容作为道具传递?

  1. 组件索引 - 索引将用于在数组中创建/更新。
  2. set function - 此函数根据组件索引将数据放入数组中。
  3. <SubObjectForm setData={this.setSubObjectData} objectIndex={index}/>

    这里{index}可以根据使用这个SubObjectForm的位置传入。

    和setSubObjectData可以是这样的。

     setSubObjectData: function(index, data){
          var arrayFromParentObject= <retrieve from props or state>;
          var objectInArray= arrayFromParentObject.array[index];
          arrayFromParentObject.array[index] = Object.assign(objectInArray, data);
     }
    

    在SubObjectForm中,可以在数据更改时调用this.props.setData,如下所示。

    <input type="text" name="name" onChange={(e) => this.props.setData(this.props.objectIndex,{name: e.target.value})}/>
    

答案 19 :(得分:0)

this.setState({
      items: this.state.items.map((item,index) => {
        if (index === 1) {
          item.name = 'newName';
        }
        return item;
      })
    });

答案 20 :(得分:0)

使用handleChange上的事件来确定已更改的元素,然后对其进行更新。为此,您可能需要更改某些属性以识别它并更新它。

请参阅小提琴https://jsfiddle.net/69z2wepo/6164/

答案 21 :(得分:0)

@JonnyBuchanan的答案非常有效,但仅适用于数组状态变量。如果状态变量只是一个字典,请遵循以下步骤:

inputChange = input => e => {
    this.setState({
        item: update(this.state.item, {[input]: {$set: e.target.value}})
    })
}

您可以将[input]替换为字典的字段名,并将e.target.value替换为其字典的值。此代码在我的表单的输入更改事件上执行更新作业。

答案 22 :(得分:0)

尝试这一点它肯定会起作用,其他情况我试过但没有工作

import _ from 'lodash';

this.state.var_name  = _.assign(this.state.var_name, {
   obj_prop: 'changed_value',
});

答案 23 :(得分:-1)

我会移动函数句柄更改并添加索引参数

handleChange: function (index) {
    var items = this.state.items;
    items[index].name = 'newName';
    this.setState({items: items});
},

到动态表单组件并将其作为prop传递给PopulateAtCheckboxes组件。当你循环你的项目时,你可以包括一个额外的计数器(在下面的代码中称为索引),以传递给句柄更改,如下所示

{ Object.keys(this.state.items).map(function (key, index) {
var item = _this.state.items[key];
var boundHandleChange = _this.handleChange.bind(_this, index);
  return (
    <div>
        <PopulateAtCheckboxes this={this}
            checked={item.populate_at} id={key} 
            handleChange={boundHandleChange}
            populate_at={data.populate_at} />
    </div>
);
}, this)}

最后,您可以调用您的更改侦听器,如下所示

<input type="radio" name={'populate_at'+this.props.id} value={value} onChange={this.props.handleChange} checked={this.props.checked == value} ref="populate-at"/>

答案 24 :(得分:-1)

尝试如下更新您的handleChange-

 handleChange: function (e) {
   const items = this.state.items;
   items[1].name = "newName";
   this.setState({items: items});
 },