When you first start programming, it seems pretty simple: create a variable, assign it a value, do stuff with that variable, and then go home. Easy peasy.
As your skill and ability increase, you will start to notice lots of nuances and details in the tools. For example, larger groups of state or data need to be managed with more advanced apps. Typically, this is done by creating or updating a structured object. But how should that object be structured? How should that data be organized? And more importantly, how does this object live in memory, and what can I expect from it?
When dealing with objects in JavaScript, how you assign them to variables or properties can have significant implications for your app's behavior. The default approach is to assign the value using the "=" operator. This is called direct assignment. However, another approach is to make a copy using one of several approaches.
While these approaches have their own names, I like to think of them this way:
- Reference - using assignment operators like
=
- Array Copy - using the spread operator
[...]
- Object Copy - using JSON functions, Lodash, or
structuredClone(obj)
When assigning objects in JavaScript, your choice can lead to different outcomes, particularly regarding how changes to the object are handled. Understanding the difference between these approaches is crucial for effective state management. Let's look at how these different approaches work.
Direct Assignment aka Reference
Direct assignment (this.list = listData;
) sets this.list
to reference the same object as listData
. That means we are just dealing with a pointer to that object in memory. So, we say that both this.list
and listData
are two different labels for the same object.
This approach has its own set of implications:
- Shared Reference:
this.list
andlistData
will refer to the same object. Any changes tothis.list
will directly affectlistData
. - Single Source of Truth: Useful when you want
this.list
to be directly linked tolistData
, maintaining consistency between them.
Example:
const listData = { a: 1, b: 2 };
this.list = listData; // References the same object
this.list.a = 3;
console.log(listData.a); // Output: 3
In this example, modifying this.list
also changes listData
, as both reference the same object. This causes lots of problems when you don't expect that behavior. However, it is still very helpful, especially when managing state. You can use block-level variables to reference specific parts of the global state easily.
When to Use This Approach:
- When you want any changes to
this.list
to be reflected inlistData
immediately. - When managing a single source of truth is crucial for your application logic.
This is why I think of this as a reference - it reminds me that this is a shared object, not a unique one.
Two types of Copy
When working with objects and arrays in JavaScript, the terms "shallow copy" and "deep copy" refer to different ways of duplicating these data structures. Understanding these concepts is vital for effective state management and avoiding unintended side effects in your code. For this reason, I like to think of these methods as either for flat (array) data or nested (object) data.
Shallow Copy aka Array Copy
A shallow copy of an object or array is a copy where only the top-level properties are duplicated. If these properties are references to other objects or arrays, the references are copied, but the nested objects or arrays are not. This means changes to the nested objects or arrays in the copied object will also affect the original object.
Example:
let original = {
name: 'Alice',
address: {
city: 'Wonderland',
zip: '12345'
}
};
// Creating a shallow copy using the spread operator
let shallowCopy = { ...original };
shallowCopy.name = 'Peter'; // This changes only the shallow copy
shallowCopy.address.city = 'Neverland'; // This changes both the shallow copy and the original
console.log(original.name); // Output: 'Alice'
console.log(original.address.city); // Output: 'Neverland'
In this example, changing shallowCopy.name
does not affect original.name
, but changing shallowCopy.address.city
does affect original.address.city
because the address
object is shared between the original and the shallow copy.
Two ways to Create a Shallow Copy:
-
Spread operator: The spread operator is the simplest way to make a shallow copy (array copy) and is a very elegant approach.
let shallowCopy = { ...original };
-
Lodash: The Lodash library provides a
_.clone
method that does the same thing. I prefer this approach because it will be easier for another developer to understand the code, especially if they aren't familiar with the spread operator in Javascript.const _ = require('lodash'); let deepCopy = _.clone(original);
Pro tip: Appsmith includes the Lodash library by default in all apps, so take advantage of it!
When to Use This Approach:
- Use when you only need to copy the top-level properties.
- You are okay with nested objects being shared.
This is often sufficient for simple state updates or when working with immutable data structures. This is why I think of it as "Array Copy" - because I only want to use the shallow copy for arrays or flat data structures. I prefer to use the Lodash method clone()
as my default because I want to make sure that the next developer understands the code, but if you are the primary maintainer, you may prefer the {...}
for aesthetic reasons.
Deep Copy aka Object Copy
A deep copy of an object or array is a copy where all levels of the object or array are duplicated. This means that all nested objects and arrays are also copied, and the new copy is completely independent of the original. Changes to any part of the copied object or array will not affect the original.
Example:
const original = {
name: 'Alice',
address: {
city: 'Wonderland',
zip: '12345'
}
};
// Creating a deep copy using a custom function or a library like Lodash
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.name = 'Peter'; // This changes only the deep copy
deepCopy.address.city = 'Neverland'; // This changes only the deep copy
console.log(original.name); // Output: 'Alice'
console.log(original.address.city); // Output: 'Wonderland'
In this example, changes to deepCopy.name
and deepCopy.address.city
do not affect the original object because deepCopy
is a completely independent copy.
Three ways to Create a Deep Copy:
-
JSON Method: One common method to create a deep copy of an object is using
JSON.stringify
andJSON.parse
. This is very simple and lightweight. However, this method has limitations, such as not supporting functions,undefined
,Infinity
,NaN
, and circular references. This is because JSON can't copy non-serializable data (e.g., functions, undefined). So those things are stripped out.let deepCopy = JSON.parse(JSON.stringify(original));
-
Lodash: The Lodash library provides a convenient
_.cloneDeep
method that handles more complex scenarios. Personally, I prefer this approach because it is cleaner and safer without building your own deep clone method.const _ = require('lodash'); let deepCopy = _.cloneDeep(original);
-
structuredClone: The
structuredClone
method provides a built-in way to create a deep copy of an object, including handling circular references and other non-serializable data. This is a more modern approach that is supported in newer JavaScript environments.let deepCopy = structuredClone(original);
When to Use This Approach:
- Use when you need a completely independent copy of the object or array
- When you need all of the nested objects.
Using a deep copy ensures that changes to the copy do not affect the original. This is crucial when dealing with complex nested structures or ensuring full independence between copies. This is why I think of it as "Object Copy" - because I want a full replication of the object. Typically, I want to use the Lodash cloneDeep()
method or structuredClone()
for this. Even though it is the biggest performance hit, I only want to use it sparingly and be absolutely sure that my objects are not shared.
Choosing the Right Approach
- Use Shallow Copy: When you need
this.list
to be independent oflistData
, preventing unintended side effects. - Use Direct Assignment: When you want
this.list
to reflect changes made tolistData
, maintaining a single source of truth.
Method | Description | Handles Nested Structures | Copies Non-Serializable Data | Performance Impact | When to Use |
---|---|---|---|---|---|
Reference Direct Assignment |
Assigns the object reference directly | ๐ซ | โ |
Very Low |
When you want changes in one object to reflect in another |
Shallow Spread Operator |
Creates a shallow copy of the object using the spread operator | ๐ซ | โ |
Low |
When you need a quick shallow copy of an object |
Shallow Lodash |
Creates a shallow copy using Lodash | ๐ซ | โ |
Low |
When using Lodash for better readability |
Deep(ish) JSON Methods |
Creates a deep copy by serializing and parsing the object | โ | ๐ซ |
Medium |
For simple objects with serializable data only |
Deep Lodash |
Creates a deep copy using Lodash | โ | โ |
High |
When you need a reliable and thorough deep copy, and you have the Lodash library installed. |
Deep
|
Creates a deep copy using native JS | โ | โ |
High |
When you need a reliable and thorough deep copy with native JS. |
Understanding the difference between referencing and copying in JavaScript is fundamental for effective data management. Choosing the right approach can help you avoid common pitfalls and ensure your application behaves as expected. Whether you need object independence or a shared reference, you have a selection of different approaches to choose from.
By grasping these concepts, you can write cleaner, more predictable code, ultimately leading to more performant and maintainable applications. Using the appropriate method for each situation will help you manage your code.