React中setState期间克隆对象/数组的正确方法

22

我开始了:

constructor() {
   super();
      this.state = {
         lists: ['Dogs','Cats'], 
         items: {Dogs: [{name: "Snoopy"}, {name: "Lola"}, {name: "Sprinkles"}], 
                 Cats: [{name: "Felidae"}, {name: "Garfiled"}, {name: "Cat in the Hat"}] }             
   };

}

然后我有我的addItem函数:

handleAddItem(s) {      

  var key = Object.keys(s)[0];
  var value = s[key];

  var allItems = {...this.state.items};

      allItems[key].push({name: value});    

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

在其他地方,我将s定义为:

var s={};
   s[this.props.idName] = this.refs.id.value;

这样可以工作,但我想知道是否这是将元素添加到items的一个键中的正确方式。AllItems 实际上指向 this.state.items,我认为它应该是一个深拷贝,但我不确定如何做到这一点。

看起来 items 是一个容纳键/值对且值是数组的对象。对吗?我在哪里可以学习如何操作这样的结构?


但似乎我们直接改变了状态,因为allItems实际上指向this.state.items。 - DCR
为什么你不直接这样写 this.state = { dogs: [], cats: [] } ?可能会让编程变得更简单。 - Andy
7个回答

40

我个人依赖于这个深拷贝策略:JSON.parse(JSON.stringify(object)),而不是 spread 操作符,因为当处理嵌套对象或多维数组时,后者容易引起奇怪的问题。

如果我没记错的话,spread 操作符并不进行深度拷贝,会导致在 React 中操作嵌套对象时发生状态变异。

请运行以下代码以更好地了解两者之间的区别。想象一下你使用 spread 操作符来改变状态变量的情况。

const obj = {Dogs: [{name: "Snoopy"}, {name: "Lola"}, {name: "Sprinkles"}], Cats: [{name: "Felidae"}, {name: "Garfiled"}, {name: "Cat in the Hat"}] };

const newObj = {...obj};
console.log("BEFORE SPREAD COPY MUTATION")

console.log("NEW OBJ: " + newObj.Dogs[0].name); //Snoopy
console.log("OLD OBJ: " + obj.Dogs[0].name); //Snoopy

newObj.Dogs[0].name = "CLONED Snoopy";

console.log("AFTER SPREAD COPY MUTATION")

console.log("NEW OBJ: " + newObj.Dogs[0].name); // CLONED Snoopy
console.log("OLD OBJ: " + obj.Dogs[0].name); // CLONED Snoopy

// Even after using the spread operator the changed on the cloned object are affected to the old object. This happens always in cases of nested objects.

// My personal reliable deep copy

console.log("*********DEEP COPY***********");

console.log("BEFORE DEEP COPY MUTATION")
deepCopyObj = JSON.parse(JSON.stringify(obj));


console.log("NEW OBJ: " + newObj.Dogs[0].name); //CLONED Snoopy
console.log("OLD OBJ: " + obj.Dogs[0].name); // CLONED Snoopy
console.log("DEEP OBJ: " + deepCopyObj.Dogs[0].name); //CLONED Snoopy


deepCopyObj.Dogs[0].name = "DEEP CLONED Snoopy";

console.log("AFTER DEEP COPY MUTATION")
console.log("NEW OBJ: " + newObj.Dogs[0].name); // CLONED Snoopy
console.log("OLD OBJ: " + obj.Dogs[0].name); // CLONED Snoopy
console.log("DEEP OBJ: " + deepCopyObj.Dogs[0].name); // DEEP CLONED Snoopy

现在,如果你想要在对象上进行深复制,请将处理程序更改为以下内容:

handleAddItem(s) {      

  var key = Object.keys(s)[0];
  var value = s[key];

  var allItems = JSON.parse(JSON.stringify(this.state.items));

      allItems[key].push({name: value});    

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

5
请注意,这可能会导致数据丢失。例如日期。 - Luiz Domingues
似乎效率不高? - PeterT
是的,这个答案简单明了,但肯定比其他方法慢。不过在很多情况下这可能并不重要。根据我找到的基准测试,Lodash的cloneDeep大约快60%。https://www.measurethat.net/Benchmarks/Show/2751/0/lodash-clonedeep-vs-json-clone - Kenmore

3

我想补充一些有关数组克隆的信息。您可以调用slice,并将0作为第一个参数:

const clone = myArray.slice(0);

上面的代码创建了原始数组的克隆;请记住,如果您的数组中存在对象,则引用将被保留;即上述代码不会对数组内容进行“深层次”的克隆。


就像接受的帖子中所说,这是行不通的。我有一个对象数组,其中的值也是对象,唯一有效的方法是接受的答案。这个切片方法仍然会在状态和“copy”两个数组中更新我的内部对象值,浪费了很多时间。 - ikiK

2

这是一个一行式的深拷贝。适用于“单层”深度对象。如果您的对象具有其他对象/数组,则需要使用递归方法。

const clone = array_of_objects.map( x => { return {...x}} )

这帮助我修复了单行的错误 非常感谢 - Kimi Raikkonen 9790

2

在 React 中设置状态时,由于您希望保持不可变性(因此始终创建新对象或克隆),而展开运算符“...”不会进行深层克隆,因此您应该使用structuredClone()

请查看 MDN:
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

全局 structuredClone() 方法使用结构化克隆算法创建给定值的深度克隆

该方法还允许将原始值中的可转移对象传输而不是克隆到新对象中。传输的对象从原始对象中分离并附加到新对象上;它们在原始对象中不再可访问。

这或多或少是当前(至少在本篇文章发布时)用于克隆的JSON.parse(JSON.stringify(yourObject))方法的现代替代品。

以下是一个函数式组件示例:

const nestedAnimals = {
    lists: ['Dogs', 'Cats'],
    items: {
        Dogs: [
            {name: "Snoopy"},
            {name: "Lola"},
            {name: "Sprinkles"}
        ],
        Cats: [
            {name: "Felidae"},
            {name: "Garfiled"},
            {name: "Cat in the Hat"}
        ]
    }
}

const someComponent = (props) => {
    const [animals, setAnimals] = useState(nestedAnimals)
    const [selectedList, setSelectedList] = useState(animals.lists[0])
    const [animalInput, setAnimalInput] = useState("")
    
    // Handler for adding a new animal to our deeply nested object.
    function addAnimal(event) {
        const newAnimal = {name: animalInput}
        // Here's where the deep cloning magic happens.
        const newAnimals = structuredClone(animals)
        newAnimals.items[selectedList].push(newAnimal)
        // Now you've updated your state in an immutable fashion.
        setAnimals(newAnimals)
    }

    // Handler for swapping lists.
    function changeList(event) {
        setSelectedList(event.target.value)
    }
    
    // Handler for storing the value from the input field as it changes.
    function changeAnimalInput(event) {
        setAnimalInput(event.target.value)
    }

    return (
        <div>
            <input type="text" value={animalInput} onChange={changeAnimalInput}/>
            <button onClick={addAnimal}>Add Animal</button>
            <select onChange={changeList}>
                {animals.lists.map(list => <option value={list}>{list}</option>)}
            </select>
            <ul>
                {animals.items[selectedList].map(animal => <li>{animal.name}</li>)}
            </ul>
        </div>
    )
}

2

一个问题可能是var allItems = {...this.state.items};只会进行浅克隆this.state.items。所以当你将数据推入这个数组时,在调用setState之前,它会改变现有的数组。

您可以使用Immutable.js来解决此问题。

import { List, fromJS, Map } from 'immutable';

constructor() {
   super();
      this.state = {
       lists: List(['Dogs','Cats']), 
       items: fromJS({
        Dogs: [
          { name: "Snoopy" },
          ...
        ],
        Cats: [
          { name: "Felidae" },
          ...
        ]
      })
   };
}

然后您的添加函数将如下所示:
handleAddItem(s) {      
  var key = Object.keys(s)[0];
  var value = s[key];

  var allItems = this.state.items.set(key, Map({ name: value }));
  this.setState({ items: allItems });
}

仅供思考!


当我尝试使用你的映射函数时,出现了“构造函数 Map 需要 'new'” 的错误提示。 - DCR
那是什么意思? - DCR
我在最顶部添加了{ Map } from 'immutable';,这样Map就不会抛出你上面提到的错误了。 - willwoo

2
所选答案是一个奇迹,非常感谢Nandu Kalidindi,我在我的项目中遇到了嵌套数组内部的错误,并且没有理解“深层”或“嵌套”数组和对象不会被复制到新对象的概念。
这是我对他的修复方案的看法,并且让它看起来更吸引人,效果也一样好!
我使用lodash,并发现他们的_.cloneDeep()函数可以满足我的更新状态需求。 Lodash _.cloneDeep() 我学到了映射更新数组防止任何错误是处理这种问题的最佳方法,但我正在克隆整个带有嵌套数组的对象,这些数组正在改变状态中旧对象的数组。
这是我的答案。

const state = {
    fields: {
        "caNyDcSDVb": {
            id: "caNyDcSDVb",
            name: "test1",
            type: "text",
            options: ["A", "B", "C"]
        }
    },
};

const FieldCopy = (id) => {
    const newCopiedField = _.cloneDeep(state.fields[id]);
    newCopiedField.id = nanoid(10);
    return {
        ...state,
        fields: {
            ...state.fields,
            newCopiedField[id]: newCopiedField
        }
    };
};

0
感谢Luke Dupin,这个解决方案对我很有帮助!
const clone = items.map( x => ({...x}) )

请不要将“谢谢”作为答案。一旦您拥有足够的声望,您就可以投票支持有用的问题和答案。- 来自审核 - Geshode

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接