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

mar*_*ins 215 javascript state reactjs

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

该组件在此处作为JSFiddle提供.

我的初始状态如下:

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>
    );
  }
Run Code Online (Sandbox Code Playgroud)

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

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>
    );
  }
});
Run Code Online (Sandbox Code Playgroud)

我该如何制作this.setState才能让它更新items[1].name

Jon*_*nan 109

您可以使用updateimmutability helper:

this.setState({
  items: update(this.state.items, {1: {name: {$set: 'updated field name'}}})
})
Run Code Online (Sandbox Code Playgroud)

或者如果您不关心能否在shouldComponentUpdate()生命周期方法中检测到对此项目的更改===,您可以直接编辑状态并强制组件重新渲染 - 这实际上与@limelights的答案相同,因为它是将对象拉出状态并进行编辑.

this.state.items[1].name = 'updated field name'
this.forceUpdate()
Run Code Online (Sandbox Code Playgroud)

编辑后添加:

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

  • 嗨,使用`update`引发了这个错误:`未捕获的ReferenceError:未定义更新` (4认同)
  • 没关系,我添加了React插件并使用了`React.addons.update`。 (2认同)
  • 我认为OP应该接受这个答案,以便当他们进入这个页面时,它会帮助新人们遵循不可变的方法. (2认同)
  • update方法还可以使用括号中的索引变量(例如[myIndex])代替上面的1,因此JavaScript知道采用该变量的值,而不是查找名为“ myIndex”的数组键。 (2认同)

Mar*_*nVK 85

错误的方法!

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

    // update state
    this.setState({
        items,
    });
};
Run Code Online (Sandbox Code Playgroud)

正如许多更好的开发人员在评论中指出的那样:改变状态是错误的!

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

所以正确的方法是:

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

  • 是不是直接调用`items [1] .role = e.target.value`变异状态? (49认同)
  • 你正在改变状态,这完全反对维持状态不变的想法,如建议的反应.在大型应用程序中,这会给您带来很多痛苦. (27认同)
  • ES6只是因为你正在使用"const"? (24认同)
  • @MarvinVK,你的答案是"所以最好的做法是:"然后使用"this.forceUpdate();" 这是不推荐的,因为它可以被setState()覆盖,请参阅https://facebook.github.io/react/docs/react-component.html#state.最好改变这一点,这对未来的读者来说并不困惑. (8认同)
  • 具体而言,改变状态的缺点是你不能在shouldComponentUpdate中执行===比较,并且当setState运行时你的状态改变可能会被覆盖.https://facebook.github.io/react/docs/react-component.html#state (4认同)
  • 只是想指出,由于编辑,许多评论现在已经不相关了,“正确的方法”实际上是正确的方法。 (4认同)
  • @JamesZ.不仅如此,这里还有很多其他答案提出了一个"解决方案"来直接改变国家,即使是最高票的国家.我应该对所有这些进行投票和评论,你也应该做得更好!并提出正确的方法.Newbs将不胜感激. (2认同)
  • @MarvinVK,你还在改变状态,不是吗?基本上:`this.state.items [1] .role = e.target.value;`.你必须知道这是错的.它有这么多的赞成,这让人非常困惑.**你应该删除这个答案.** (2认同)

mpe*_*pen 83

由于此线程中存在大量错误信息,因此无需帮助库就可以执行以下操作:

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});
},
Run Code Online (Sandbox Code Playgroud)

如果需要,您可以组合步骤2和3:

let item = {
    ...items[1],
    name: 'newName'
}
Run Code Online (Sandbox Code Playgroud)

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

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

注意:我做了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
Run Code Online (Sandbox Code Playgroud)

  • @Evmorov 因为它不是深度克隆。第一步只是克隆数组,而不是里面的对象。换句话说,新数组中的每个对象仍然“指向”内存中的现有对象——改变一个将改变另一个(它们是一个并且相同)。另请参阅底部终端示例中的 `items[0] === clone[0]` 位。三重 `=` 检查对象是否指向同一事物。 (5认同)
  • @TranslucentCloud哦是的,辅助方法绝对不错,但我想每个人都应该知道引擎盖下发生了什么:-) (3认同)
  • @IanWarburton 不是。[`items.findIndex()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) 应该对此进行简短的工作。 (3认同)

Tra*_*oud 47

若要修改作出反应的状态深深嵌套的对象/变量,通常使用三种方法:香草JS Object.assign,不变性辅助cloneDeepLodash.还有很多其他不太受欢迎的第三方库来实现这一目标,但在这个答案中,我将只介绍这三个选项.此外,还有一些方法只使用vanilla JavaScript,比如数组传播(参见@ mpen的答案),但它们不是非常直观,易于使用并且能够处理所有状态操作情况.

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

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

鉴于这种状态结构:

state = {
    outer: {
        inner: 'initial value'
    }
}
Run Code Online (Sandbox Code Playgroud)

1. Vanilla JavaScript的Object.assign

const App = () => {
  const [outer, setOuter] = React.useState({ inner: 'initial value' })

  React.useEffect(() => {
    console.log('Before the shallow copying:', outer.inner) // initial value
    const newOuter = Object.assign({}, outer, { inner: 'updated value' })
    console.log('After the shallow copy is taken, the value in the state is still:', outer.inner) // initial value
    setOuter(newOuter)
  }, [])

  console.log('In render:', outer.inner)

  return (
    <section>Inner property: <i>{outer.inner}</i></section>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('react')
)
Run Code Online (Sandbox Code Playgroud)

请记住,这inner 不会执行深度克隆,因为它只复制属性值,这就是为什么它的作用称为浅复制(请参阅注释).

对象属性是此对象的顶级(outer.inner),它们的值可以是基元(字符串,数字等)或对其他对象(对象,数组)的引用.

因此,如果您具有相对简单的一级深度状态结构,其中最内层成员保持基本类型的值,则它将起作用.就像const newOuter...在这个例子中一样,持有字符串.

如果您有更深的对象(第二级或更高级别),您应该更新,请不要使用Object.assign.你冒险直接改变国家.

2. Lodash的cloneDeep

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

<main id="react"></main>
Run Code Online (Sandbox Code Playgroud)

基本上,Lodash {}执行深度克隆,因此如果您具有包含多级对象或数组的相当复杂的状态,则它是一个强大的选项.

3. 不变性帮助者

const App = () => {
  const [outer, setOuter] = React.useState({ inner: 'initial value' })

  React.useEffect(() => {
    console.log('Before the deep cloning:', outer.inner) // initial value
    const newOuter = _.cloneDeep(outer) // cloneDeep() is coming from the Lodash lib
    newOuter.inner = 'updated value'
    console.log('After the deeply cloned object is modified, the value in the state is still:', outer.inner) // initial value
    setOuter(newOuter)
  }, [])

  console.log('In render:', outer.inner)

  return (
    <section>Inner property: <i>{outer.inner}</i></section>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('react')
)
Run Code Online (Sandbox Code Playgroud)

关于不变性辅助很酷的事情是,它可以outer值状态的项目,也{ inner: 'initial value' },{ inner: 'updated value' },newOuter(等)它们.这是可用命令列表.

旁注

请注意,{ inner: 'updated value' }它只修改状态对象的第一级属性(inner在本例中为属性),而不是深层嵌套(newOuter).如果它以另一种方式表现,那么这个问题就不存在了.

顺便说一下,setOuter()这只是一个简写outer.而newOuter之后的outer是之后被立即执行的回调state = { outer: { inner: { innerMost: 'initial value' } } }已设置的状态.方便的话,如果你需要在它完成工作后做一些事情(在我们的情况下,在它设置后立即显示状态).

性能

现在,当涵盖所有三种主要方法时,让我们比较它们的速度.我为此创建了一个jsperf.在此测试中,具有10000个具有随机值的属性的巨大状态由上述所有三种方法操纵.

以下是我在Windows 10上使用Chrome 61.0.3163的结果:

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

<main id="react"></main>
Run Code Online (Sandbox Code Playgroud)

请注意,在同一台机器上的Edge中,Lodash的表现比其他两种方法差.在Firefox中,有时候newOuter会成为胜利者,但在其他时候却是胜利者outer.

这是jsperf.com测试链接.

哪一个是正确的为您的项目?

如果您不想或可以使用外部依赖项,并且具有简单的状态结构,请坚持使用Object.assign.

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

如果您需要高级功能,即如果您的状态结构很复杂并且您需要对其执行各种操作,请尝试newOuter,它是一种非常先进的工具,可用于状态操作.


小智 29

我有同样的问题.这是一个有效的简单解决方案!

const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });
Run Code Online (Sandbox Code Playgroud)

  • @TranslucentCloud - 这肯定不是直接变异.克隆,修改原始数组,然后使用克隆数组再次设置状态. (11认同)
  • @TranslucentCloud-在此之前,我的编辑与之无关,非常感谢您的态度。@Jonas在这里的答案仅是一个简单的错误,使用的是大括号“ {”而不是我已经纠正的括号。 (2认同)

Hen*_*son 27

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

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

   this.setState({items: items});
}
Run Code Online (Sandbox Code Playgroud)

  • @HenrikAndersson东西似乎不就在你的例子.`items`没有在任何地方定义. (10认同)
  • 这是一个典型的React反模式,你将`item`赋给状态自己的`this.state.items [1]`变量.然后你修改`item`(`item.name ='newName'`),从而直接改变状态,这是非常不鼓励的.在你的例子中,甚至不需要调用`this.setState({items:items})`,因为状态已经直接变异了. (6认同)
  • 不,它会产生`Uncaught TypeError:无法读取null`的属性'items'. (3认同)

eic*_*ksl 27

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

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

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

  • 如果您使用 ES6,这是合适的答案。@Jonas 的回答是内联的。然而,由于解释,它脱颖而出。 (2认同)

小智 22

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

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

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

  • 你的例子中的`items`是什么?你是说`this.state.items`还是别的什么? (8认同)

jam*_*mes 11

有时在React中,改变克隆数组可能会影响原始数组,这种方法永远不会导致变异:

    const myNewArray = Object.assign([...myArray], {
        [index]: myNewItem
    });
    setState({ myArray: myNewArray });
Run Code Online (Sandbox Code Playgroud)

或者,如果您只想更新项目的属性:

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


San*_*ero 6

由于上述选项对我来说都不理想,因此我最终使用了地图:

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


小智 5

在一行中使用带箭头函数的数组映射

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

  • 这与大约一年前发布的[另一个答案](/sf/answers/3918917271/)基本相同 (2认同)