React:如何使用 setState 更新 state.item[1] 状态?

IT技术 javascript reactjs state
2021-02-07 04:36:33

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

该组件可在此处作为 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>
    );
  }

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

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

6个回答

由于此线程中有很多错误信息,因此您可以在没有帮助库的情况下执行以下操作:

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
@TranslucentCloud 哦,是的,辅助方法绝对不错,但我认为每个人都应该知道引擎盖下发生了什么:-)
2021-03-16 04:36:33
非常感谢。我正在努力更新状态数组的元素。你节省了我很多时间。
2021-03-19 04:36:33
为什么我们需要第 2 步和第 3 步?为什么我们不能直接变异第一步后的克隆?
2021-03-23 04:36:33
@IanWarburton 不是。items.findIndex()应该做简短的工作。
2021-03-25 04:36:33
@Evmorov 因为它不是深度克隆。第一步只是克隆数组,而不是里面的对象。换句话说,新数组中的每个对象仍然“指向”内存中的现有对象——改变一个将改变另一个(它们是一个并且相同)。另请参阅items[0] === clone[0]底部终端示例中的位。三重=检查对象是否指向同一事物。
2021-04-07 04:36:33

您可以update为此使用不变性助手

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

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

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

编辑后补充:

查看react-training 中简单组件通信课程,了解如何将回调函数从保持状态的父组件传递到需要触发状态更改的子组件的示例。

非常感谢,Jonny,浪费了一天,终于得到了解决方案。
2021-03-12 04:36:33
阅读此答案:stackoverflow.com/a/51018315/2396744,我认为您仍然需要使用 prevstate 回调的 setstate 函数的更新程序形式。
2021-03-13 04:36:33

错误的方法!

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,
        },
    }));
};
您正在改变状态,这完全违背了像 react 建议的那样保持状态不可变的想法。在大型应用程序中,这会给您带来很多痛苦。
2021-03-13 04:36:33
只是想指出,由于编辑,现在很多评论都无关紧要,正确的方式实际上是正确的方式。
2021-03-18 04:36:33
@MarvinVK,你的回答是“所以最好的做法是:”然后使用“this.forceUpdate();” 不推荐这样做,因为它可能会被 setState() 覆盖,请参阅facebook.github.io/react/docs/react-component.html#state最好改变这一点,这样未来的读者就不会感到困惑。
2021-03-24 04:36:33
不是items[1].role = e.target.value直接调用变异状态吗?
2021-03-26 04:36:33
ES6 只是因为你使用了“const”?
2021-04-04 04:36:33

为了在 React 状态中修改深度嵌套的对象/变量,通常使用三种方法:vanilla JavaScript's Object.assignimmutability -helpercloneDeep来自Lodash

还有很多其他不太流行的第三方库可以实现这一点,但在这个答案中,我将只介绍这三个选项。此外,还存在一些额外的原生 JavaScript 方法,例如数组传播(例如,请参阅 @mpen 的回答),但它们不是很直观、易于使用且无法处理所有状态操作情况。

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

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

鉴于此状态对象结构:

state = {
    outer: {
        inner: 'initial value'
    }
}

您可以使用以下方法更新最内层inner字段的值,而不会影响状态的其余部分。

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

请记住,Object.assign 不会执行深度克隆,因为它只复制属性值,这就是为什么它被称为浅复制(见评论)。

为此,我们应该只操作原始类型 ( outer.inner)的属性,即字符串、数字、布尔值。

在这个例子中,我们创建一个新的常数(const newOuter...),使用Object.assign,它创建一个空的对象({}),拷贝outer对象({ inner: 'initial value' })到它,然后复制一个不同的对象{ inner: 'updated value' } 吧。

这样,最终新创建的newOuter常量将保留一个值,{ inner: 'updated value' }因为该inner属性被覆盖了。newOuter是一个全新的对象,它没有链接到状态中的对象,因此可以根据需要对其进行变异,并且状态将保持不变并且在运行更新它的命令之前不会更改。

最后一部分是使用setOuter()setterouter用一个新创建的newOuter对象替换原来在state中的(只会改变值,不会改变属性名outer)。

现在想象我们有一个更深的状态,比如state = { outer: { inner: { innerMost: 'initial value' } } }我们可以尝试创建newOuter对象并用outer状态中内容填充它,但由于嵌套太深,Object.assign无法将innerMost的值复制到这个新创建的newOuter对象innerMost

你仍然可以复制inner,就像上面的例子一样,但是因为它现在是一个对象而不是一个原始对象引用fromnewOuter.inner将被复制到outer.inner代替,这意味着我们最终将本地newOuter对象直接绑定到状态中的对象.

这意味着在这种情况下,本地创建的突变newOuter.inner将直接影响outer.inner对象(在状态中),因为它们实际上变成了相同的东西(在计算机的内存中)。

Object.assign 因此,只有当你有一个相对简单的一级深度状态结构,最里面的成员保存原始类型的值时才会起作用。

如果您有更深的对象(第 2 级或更多),您应该更新,请不要使用Object.assign. 你冒着直接改变状态的风险。

2. Lodash 的 cloneDeep

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

Lodash 的cloneDeep使用起来更简单。它执行深度克隆,因此它是一个强大的选项,如果您有一个相当复杂的状态,其中包含多级对象或数组。只是cloneDeep()顶级状态属性,以任何你喜欢的方式改变克隆的部分,然后setOuter()它回到状态。

3.不变性助手

const App = () => {
  const [outer, setOuter] = React.useState({ inner: 'initial value' })
  
  React.useEffect(() => {
    const update = immutabilityHelper
    console.log('Before the deep cloning and updating:', outer.inner) // initial value
    const newOuter = update(outer, { inner: { $set: 'updated value' } })
    console.log('After the cloning and updating, 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')
)
<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://wzrd.in/standalone/immutability-helper@3.0.0"></script>

<main id="react"></main>

immutability-helper把它带到一个全新的水平,它很酷的事情是它不仅可以用$set值来说明项目,还可以对它们进行$push, $splice, $merge(等)。这是可用命令列表

附注

再次记住,setOuter这只修改状态对象第一级属性outer在这些示例中),而不是深度嵌套的 ( outer.inner)。如果它以不同的方式表现,这个问题就不会存在。

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

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

如果你操纵一个巨大和/或复杂的 state,LodashcloneDeep是一个明智的选择。

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

……或者,你真的需要这样做吗?

如果你在 React 的状态下持有一个复杂的数据,也许现在是考虑其他处理它的方式的好时机。在 React 组件中设置一个复杂的状态对象并不是一个简单的操作,我强烈建议考虑不同的方法。

很可能您最好不要将复杂数据保存在 Redux 存储中,使用 reducer 和/或 sagas 将其设置在那里,并使用选择器访问它。

你是对的,1最里面的对象 ( a.c.d)的值会发生变化。但是,如果您将 的第一级后继重新分配b,如下所示:b.c = {f: 1}, 的相应部分a将不会发生变异(它将保留{d: 1})。无论如何,不​​错的收获,我会立即更新答案。
2021-03-14 04:36:33
是的,Object.assign在答案中明确提到了肤浅
2021-03-30 04:36:33
Object.assign不执行深拷贝。说,如果a = {c: {d: 1}}b = Object.assign({}, a),那么你执行b.c.d = 4,然后a.c.d是变异的。
2021-04-03 04:36:33
您定义的实际上是 ashallow copy而不是deep copy很容易混淆是什么shallow copy意思。shallow copy, 中a !== b,但是对于源对象中的每个键aa[key] === b[key]
2021-04-10 04:36:33
JSON.parse(JSON.stringify(object))也是深度克隆的变种。cloneDeep不过性能比 lodash 差measurethat.net/Benchmarks/Show/2751/0/...
2021-04-11 04:36:33

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

const newItems = [...this.state.items];
newItems[item] = value;
this.setState({ items:newItems });
@TranslucentCloud - 在此之前,我的编辑与此无关,非常感谢您的态度。@Jonas 在这里只在他的回答中犯了一个简单的错误,使用{花括号而不是我已经纠正的括号
2021-03-28 04:36:33
@vsync 是的,现在在您编辑原始答案之后,这根本不是突变。
2021-04-06 04:36:33
@TranslucentCloud - 这肯定不是直接突变。原始数组被克隆、修改,然后使用克隆的数组再次设置状态。
2021-04-08 04:36:33