25

I start with:

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"}] }             
   };

}

then I have my addItem function:

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

elsewhere I define s as:

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

This works but I'm wondering if this is the right way to add an element into one of the keys in items. AllItems is really pointing to this.state.items and I thought it should be a deep copy but I'm not sure how to do that.

It seems like items is an object that holds key/value pairs where the value is an array. Is that correct? Where can I go to learn how to manipulate a structure like that?

2
  • but it seems like we are directly changing state since allItems is really pointing to this.state.items Commented Dec 3, 2017 at 22:53
  • Any reason why you don't just do this.state = { dogs: [], cats: [] }. Might make life a little easier. Commented Dec 3, 2017 at 22:56

7 Answers 7

43

I personally rely on this deep copy strategy. JSON.parse(JSON.stringify(object)) rather than spread operator because it got me into weird bugs while dealing with nested objects or multi dimensional arrays.

spread operator does not do a deep copy if I am correct and will lead to state mutations with NESTED objects in React.

Please run through the code to get a better understanding of what is happening between the two. Imagine that is the state variable that you mutate using spread operator.

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

Now, if you wanted to do a deep copy on your object change the handler to this

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});
}
Sign up to request clarification or add additional context in comments.

4 Comments

Be aware that this could lead to data loss. Dates for example.
seems inefficient?
Yes, this answer is simple and clean but it's definitely slower than some other methods. That may not matter in a lot of cases, though. According to this benchmark I found Lodash cloneDeep is about 60% faster. measurethat.net/Benchmarks/Show/2751/0/…
use structuredClone builtin function to avoid creating the intermediate json string
3

I wanted to add a bit more info about cloning arrays. You can call slice, providing 0 as the first argument:

const clone = myArray.slice(0);

The code above creates clone of the original array; keep in mind that if objects exist in your array, the references are kept; i.e. the code above does not do a "deep" clone of the array contents.

1 Comment

Like said in the accepted post this will NOT work, I have an array of objects with values that are also objects, and the ONLY thing that worked properly is the accepted answer. This slice method was STILL updating my inner object vales in BOTH arrays, state, and 'copy', lost hours on this.
3

The chosen answer was a miracle and huge props to Nandu Kalidindi, I was having bugs with nested arrays inside of objects that I was updating in my project and was not understanding the concept of "deep" or "nested" arrays and objects not being copied to a new object.

This is my take on his fix and made it look a little more appealing to me and works just as good!

I utilize lodash and found their _.cloneDeep() function to serve my updating state needs.

Lodash _.cloneDeep()

I learned that mapping out updated arrays preventing any bugs was the best way to treat that kind of problem, but I was cloning entire objects with nested arrays that were mutating the old object's arrays in the state.

This is my answer.


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

Comments

3

Here is a one line deep-copy. This is for 'single level' deep objects. If your object has other objects / arrarys, you'll need to use a recursive method.

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

1 Comment

This helped me fix the error in single line Thanks a lot
3

Since you want to maintain immutability when setting your state within React (so, always make a new object or clone), and since the spread ... operator doesn't clone deeply, you should make use of structuredClone().

Take a look at the MDN:
https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

The global structuredClone() method creates a deep clone of a given value using the structured clone algorithm.

The method also allows transferable objects in the original value to be transferred rather than cloned to the new object. Transferred objects are detached from the original object and attached to the new object; they are no longer accessible in the original object.

This is more or less the modern replacement (as of this post at least) for the JSON.parse(JSON.stringify(yourObject)) method of cloning.

Here's a functional component example:

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

Comments

2

One issue might be that var allItems = {...this.state.items}; will only do a shallow clone of this.state.items. So when you push data into this array, it will change the existing array before you call setState.

You could use Immutable.js to solve this issue.

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

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

and then your add function would be as follow:

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

Just a thought!

3 Comments

when I try your map function I get Constructor Map requires 'new'
what does that mean?
i added { Map } from 'immutable'; in the very top line so that Map doesn't throw the error you mentioned above
1

Thanks to Luke Dupin, this solution helped me!

const clone = items.map( x => ({...x}) )

1 Comment

Please don't add "thank you" as an answer. Once you have sufficient reputation, you will be able to vote up questions and answers that you found helpful. - From Review

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.