Exploring React Shallow Copy Methods: Spread Operator vs Direct Assignment

64 Views Asked by At

I am currently working on a React project and encountered an interesting issue related to shallow copying objects. In my component, I have a form where I collect data, and I noticed that different methods of shallow copying the data object result in different behaviors.

Here are the three approaches I've tried:

Approach 1 (works)

const handleSubmit = async (e) => {
    e.preventDefault();
    actions.updateEmail(...Object.values(localEmailData));
    console.log(store);
};

Approach 2 (works but with console error)

const handleSubmit = async (e) => {
    e.preventDefault();
    actions.updateEmail(...localEmailData);
    console.log(store);
};

The console error reads:

Uncaught (in promise) TypeError: Invalid attempt to spread non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.

        

Approach 3 (won't work)

const handleSubmit = async (e) => {
    e.preventDefault();
    actions.updateEmail(localEmailData);
    console.log(store);
};

Can someone explain the differences between these methods, and why Approach 2, although working, triggers a console error? Additionally, why doesn't Approach 3 work?

My React component

    […]
    
    export const Home = () => {
        const { store, actions } = useContext(Context);
    
        const [localEmailData, setLocalEmailData] = useState({
            projectName: store.email.nombreDelProyecto,
            unsubscribeUrl: store.email.unsubscribeUrl,
            header: {
                link1: store.email.header.link1,
                image1: store.email.header.image1,
                alt1: store.email.header.alt1,
                link2: store.email.header.link2,
                image2: store.email.header.image2,
                alt2: store.email.header.alt2,
                link3: store.email.header.link3,
                image3: store.email.header.image3,
                alt3: store.email.header.alt3,
            },
            cta: {
                image: store.email.cta.image,
                link: store.email.cta.link,
            },[…]
});

        const handleInputChange = (e) => {
            setLocalEmailData({ localEmailData, [e.target.name]: e.target.value });
            console.log("localEmailData", localEmailData);
            console.log("Submitting form with data:", localEmailData);
        };
    
        const handleSubmit = async (e) => {
            e.preventDefault();
            actions.updateEmail(...Object.values(localEmailData));
            console.log(store);
        };
    
        return (
            <div className="mt-5">
                <div className="d-flex justify-content-center">
                    {/* Formulario de encabezado */}
                    <Form className="w-50" onSubmit={handleSubmit}>
                        […]
                        <div className="d-flex justify-content-center">
                            <Button type="submit" className="generar-correo">
                                Validate & Generate
                            </Button>
                            <Link to="/pocuromailbuilder" className="generar-correo">
                                <span>Go to </span>&nbsp;<span style={{ fontStyle: 'italic' }}>output</span>
                            </Link>
                        </div>
                    </Form>
                </div>
            </div>
        );
    
    };

Also, my action at flux.js is:

updateEmail: (newData) => {
                setStore({
                    ...getStore(),
                    email: {
                        ...getStore().email,
                        ...newData,
                    }
                });
            },

Any insights or explanations would be greatly appreciated. Thank you!

2

There are 2 best solutions below

1
On BEST ANSWER

Okay! Let's start with the analysis (Caution: may have a lot to read)

Approach 1:

Consider an object,

let testObject = {
key1:value1,
key2:value2,
key3:value3
}
// When Object.values(testObject) is applied it returns an array with object's 
// Property Values eg: [value1,value2,value3]
// and when spread operator is applied, it expands the array and throws the 
// elements out eg: ...[value1, value2, value3] becomes value1 value2 value3

Three places that accept the spread syntax are Function Arguments, Array and Object Literals. Only iterable values (array or string) can go into the first two places ie., Function Arguments and Array Literals.

Approach 2:

Now you know that there are only 3 places we could use spread syntax. Like your second example if I apply,

console.log(...testObject) // expands the object and each property is printed
// The catch here is that the console log function takes the expanded values
// and logs them, returns void and the spread operation expanded the values thinking that the
// values goes into any of the 3 places mentioned above. **But it did not hence error.**

adding {...testObject}, object literal is valid OR use [Symbol.iterator]

Now you may connect this to conclude the Approach 3 which passes your complete object.

Hope this helps!

0
On

I guess that I've understood a bit more on how objects behave with this experience, which I believe that can be summarized like so:

I've got an object:

let object = {
key1:"blue",
key2:"red",
key3:"yellow"
}

Initially, if I copy it either with a spread operator or without it, I've got the very same object:

let copiedObjectWithSpreadOperator = {...object};
let copiedObjectWithoutSpreadOperator = object

console.log("Object Copied With Spread Operator", copiedObjectWithSpreadOperator)

console.log("Object Copied Without Spread Operator", copiedObjectWithoutSpreadOperator)

This is, in both cases, console yields:

Object Copied With Spread Operator { key1: 'blue', key2: 'red', key3: 'yellow' }
Object Copied Without Spread Operator { key1: 'blue', key2: 'red', key3: 'yellow' }

And, when copied with Object.Values...

let copiedObjectWithObjectValues = Object.values(object)

console.log("Object Copied With Object.Values()", copiedObjectWithObjectValues)

Console yields...

Object Copied With Object.Values() [ 'blue', 'red', 'yellow' ]

Now, the key to all the understanding I think I've achieved come from changing the value of a property like so:

object.key1 = "green"

Now, when repeating the same console.logs:

console.log("Original Object:", object)
console.log("Object Copied With Spread Operator", copiedObjectWithSpreadOperator)
console.log("Object Copied Without Spread Operator", copiedObjectWithoutSpreadOperator)
console.log("Object Copied With Object.Values()", copiedObjectWithObjectValues)

Console yields...

Original Object: { key1: 'green', key2: 'red', key3: 'yellow' }
Object Copied With Spread Operator { key1: 'blue', key2: 'red', key3: 'yellow' }
Object Copied Without Spread Operator { key1: 'green', key2: 'red', key3: 'yellow' }
Object copied with Object.Values() [ 'blue', 'red', 'yellow' ]

We can see how the original object has changed, as so has done the one copy made without spread operator. This is, is that copy is not even a copy, is just a new reference that points to the original object.

On the other hand, the copy made with the spread operator hasn't changed when changing a value of the initial object as it is a new object itself.

Now, it all makes an awful lot of sense.

Thanks!