No history yet

Spread and Shallow Copying

Transcript

Beau

Okay, Jo, so last time we really hammered on the idea that directly mutating objects is, like, public enemy number one in JavaScript, especially with React.

Jo

Right. Because when you change that one object, you're changing it for everything that holds a reference to it. It's like secretly repainting a library book while ten people are trying to read it.

Beau

Exactly. So, the solution everyone reaches for, myself included, is the spread operator. The three little dots. You just... sprinkle that on your object, and poof, problem solved. Right? New object, no more shared references, life is good.

Jo

Life is... better. But not always as good as we think. The spread operator isn't a magic wand; it's a photocopier. And it's a very specific, kinda lazy photocopier.

Beau

A lazy photocopier? Okay, I'm intrigued. What does that mean?

Jo

So when you spread an object, say `const newObject = { ...oldObject }`, what the JavaScript engine is doing is creating a brand new, empty object at a new memory address. Then, it goes through `oldObject` key by key and copies the *values* over.

Beau

Okay, that sounds like a... a non-lazy copy to me. It's doing work.

Jo

It is. But think about what we learned about the stack and heap. If a value is a primitive, like a number or a string, it copies the actual value. So if `oldObject.id` was 42, `newObject.id` gets its own value of 42. But... what if a value is an object or an array?

Beau

Oh... wait. The 'value' is just a pointer, right? A memory address.

Jo

Exactly. The photocopier doesn't follow that pointer to the heap and copy the entire object it finds there. It just copies the pointer itself. So both your `oldObject` and your `newObject` end up pointing to the *exact same* nested object in memory.

Beau

Ah, so it’s a 'shallow' copy. It only copies the top level. That's the laziness.

Jo

Precisely. It creates a new reference for the top-level object, but it reuses, or shares, the references for any nested objects. That's why this concept is also sometimes called structural sharing. You're sharing the internal structure.

Beau

Okay, mental movie time. Let's say I have a user state object. It has a `name` property, which is a string, and a `taxDetails` property, which is another object containing things like `filingStatus` and `dependents`.

Jo

Perfect example.

Beau

If I spread that user state into a new object and then I change the `name` on the new one... the old one is unaffected, because `name` is a primitive. The value was copied.

Jo

Correct. You have two separate strings now.

Beau

But... if I change `newObject.taxDetails.filingStatus` from 'single' to 'married'... what happens to the original object?

Jo

The original `userState` object's `taxDetails` also changes. Because both of them are holding a pointer to the *one* `taxDetails` object that exists in memory. You've just mutated it through your new copy.

Beau

And that's the pitfall. You think you're being safe by spreading, but you're still creating those side effects one level deep. That's a huge source of bugs.

Jo

It's one of the most common ones. You update one piece of state and suddenly a completely unrelated part of your application breaks, because they were unknowingly sharing a nested object reference.

Beau

Okay, so question. Before the spread operator was everywhere, people used `Object.assign`. Is it doing the same thing? Is it also a lazy photocopier?

Jo

Yes, functionally it produces the exact same result. `Object.assign({}, oldObject)` is another way to make a shallow copy. The spread syntax is just newer, more declarative, and generally easier to read.

Beau

So there's no magic performance difference or anything? The engine isn't doing something smarter with the three dots?

Jo

For this use case, not really. They are both highly optimized, but their core job is the same: create a new object and copy the top-level property values. One isn't 'deeper' than the other. Both will copy the pointer to your `taxDetails` object, not the object itself.

Beau

So the takeaway is, if you have nested objects, spreading the parent is only the first step. If you want to change something inside the nested object, you have to spread *that* one too.

Jo

Exactly. You have to create a new copy at every level you intend to change. So you'd spread the user, but for the `taxDetails` property, its new value would be a spread version of the old `taxDetails`.

Beau

Which can get pretty nested and messy-looking pretty fast.

Jo

It can. And that's why understanding this behavior is so crucial. It informs how you structure your state. Sometimes, if you find yourself spreading three or four levels deep, it might be a sign that your state is too nested and could be flattened out.

Beau

So the spread operator isn't just a tool, it's also kind of a... a diagnostic. A code smell detector.

Jo

That's a great way to put it. If your update logic looks like a Russian nesting doll of spread operators, pause and think about the shape of your data. The 'lazy photocopier' is telling you something.