DeepCopy Object in JavaScript using immer - javascript

I am using immer to transform react/redux state. Can I also use immer to just deep copy an object without transforming it?
import produce, {nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}
This is from the official immer README. Does this mean that passing an empty function to produce returns a deep copy of the original state or actually the original state?
Thank you very much for your help :)!

This is easily testable with
import { produce } from 'immer'
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {})
nextState.hello = 'new world'
console.log(state, nextState)
which outputs
Object { hello: "new world" }
Object { hello: "new world" }
which means that it does NOT create a deep copy of an object.
UPDATE:
So I got interested and tested out the library a lot and here are my findings.
The code snippet I wrote above is simply an optimisation in the library which returns the old state if no changes are made. However, if you make some changes, then the library starts functioning as intended and the mutation later is made impossible. That is,
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {
draft.hello = 'new world';
})
nextState.hello = 'newer world';
console.log(state, nextState)
will result in an error: TypeError: "world" is read-only
Which means that your newState is immutable and you can no longer perform mutations on it.
Another rather interesting thing I found is that immer fails when using class instances. That is,
class Cls {
prop = 10;
}
const instance = new Cls();
const obj = {
r: instance,
};
const newObj = produce(obj, draft => {
draft.r.prop = 15;
});
console.log(obj, newObj);
results in
r: Object { prop: 15 }
r: Object { prop: 15 }
So to get back to the initial question, can you get a deep copy of the initial Object by changing nothing in the draft. No you cannot, and even if you did (by changing a property created just to fool immer perhaps), the resultant cloned object will be immutable and not really helpful.

Solution :
The Immer's produce only provides a new deep cloned object on updation.
you can create your own produce function that behaves just like that of immer's produce but gives a cloned object everytime using loadash
import _ from 'lodash';
export default function produceClone(object, modifyfunction) {
let objectClone = _.cloneDeep(object);
if (!modifyfunction) return objectClone;
modifyfunction(objectClone);
return objectClone;
}
This will give you a deepCloned(or deep copied) object everytime, irrespective of whether you modify the object or not.

Related

Attempted to assign to readonly property

first of all i get my redux array then in my_function copy that into new variable like below :
let transactions_list = useSelector(state => state.transactions_list.value);
let new_transactions_list = [...transactions_list];
when i want to change my new_transactions_list very deeply i got the error
const my_function = () => {
let new_transactions_list = [...transactions_list];
new_transactions_list[yearIndex].data_yearly[monthIndex].data_monthly.push(new_obj);
}
but when i define an array in class(without redux), it's work
Even if you are using the spreading [...transactions_list], you are still only copying the first level of the array, which means that the object below that array is still the same one that redux uses.
You have 2 options:
This is how redux recommends you to update nested object link
function updateVeryNestedField(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue
}
}
}
}
}
Or you can use something like immer, which will allow you to update your object even with immutable like this
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
Either way, you will have to update your redux state afterward since this change will only be local in your code and not the global redux.

Unable to use structuredClone() on value of ref variable

I want to make use of the structuredClone() function inside my Vue app. I want to use this to create a deep clone ( instead of using workarounds like stringify and parse or external libraries ). Inside my setup function the following code is fine
const a = {
foo: {
bar: "+"
}
};
const b = structuredClone(a);
console.log(b);
But it is not possible for me to use it on values of ref variables. This example code
import { ref } from "vue";
const a = ref({ foo: { bar: "+" } });
const b = structuredClone(a.value);
throws the error
Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': # could not be cloned.
The same goes for items from ref arrays
import { ref } from "vue";
const a = ref([{ foo: { bar: "+" } }]);
for (const b of a.value) {
const c = structuredClone(b);
}
How can this be fixed?
The error means that structuredClone was executed on Proxy instance, which cannot be cloned. In order to allow this, it should be used on raw object that a proxy wraps:
const b = structuredClone(toRaw(a.value));
Notice that toRaw is used on a.value because both a and a.value are reactive objects, and toRaw works shallowly and needs to be applied to the innermost object.
Since ref and reactive allow to compose reactive objects, toRaw still may not work for them due to how it works:
ref({ foo: { bar: barRef } })
This would require to recursively use toRaw on reactive objects before using structuredClone. At this point this doesn't make it easier than cloning the objects manually, unless more exotic objects like Set, Map, etc are in use.

ES6 object cloning using spread operator is modifying input too

I have a fairly deep interface declared that looks something like this:
export interface Job {
JobId: JobId; // type JobId = string
UserId: UserId; // type UserId = string
JobName: string;
AudioFile: JobAudioFile; // this is an interface
Status: JobStatus; // this is an enum
Tracks: JobTracks[]; // 'JobTracks' is an enum
Results: JobResults; // this is an interface
Timestamps: JobTimestamps // interface
}
Most of the members of this interface are themselves interfaces, with the general architecture following this pattern of using enums, strings, arrays and more interfaces. All code is written as TypeScript, transpiled down to JS and uploaded to AWS as JS. (Node 8.10 is running on AWS)
At one point in the code, I need to make a deep copy of a Job instantiation which was passed in as a function parameter:
export const StartPipeline: Handler = async (
event: PipelineEvent
): Promise<PipelineEvent> => {
console.log('StartPipeline Event: %o', event);
const newBucket = await copyToJobsBucket$(event.Job);
await deleteFromOriginalBucket$(event.Job);
console.log(`Job [${event.Job.JobId}] moved to Jobs bucket: ${newBucket}`);
event.Job.AudioFile.Bucket = newBucket;
event.Job.Status = Types.JobStatus.Processing;
// update the job status
// VVV PROBLEM OCCURS HERE VVV
const msg: Types.JobUpdatedMessage = new Types.JobUpdatedMessage({ Job: Object.assign({}, event.Job) });
await Send.to$(event.Job.UserId, msg);
return { ...event };
};
The definition of the JobUpdatedMessage:
export class JobUpdatedMessage extends BaseMessage {
constructor(payload: { Job: Types.Job }) {
console.log('Incoming: %o', payload);
const copy: object = { ...payload.Job };
// VVV PROBLEM ON NEXT LINE VVV
const filtered = JobUtils.FilterJobProperties(copy as Types.Job);
super(MessageTypes.JobUpdated, filtered);
}
}
The problem is after the call to JobUtils.FilterJobProperties, payload.Job has also been mutated in an undesirable and unexpected way.
Here's the implementation of JobUtils.FilterJobProperties:
export const FilterJobProperties = (from: Types.Job): Types.Job => {
const fieldsToRemove: string[] = [
'Transcripts.GSTT',
'Transcripts.WSTT',
'Transcripts.ASTT',
'TranscriptTracks',
'Transcripts.Stream.File',
'Transcripts.Stream.State',
'AudioFile.Bucket',
'AudioFile.S3Key',
];
let job: Types.Job = { ...from }; // LINE ONE
fieldsToRemove.forEach(field => _.unset(job, field)); // LINE TWO
return job;
};
(I'm using the lodash library here)
The line market 'LINE TWO' is also mutating the from function parameter, even though on 'LINE ONE' I'm doing what I think is a deep clone of from.
I know that this is the case because if I change 'LINE ONE' to:
// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));
... everything works as expected. from is not mutated, the resulting JobUpdatedMessage is as expected, and StartPipeline's event parameter doesn't have a bunch of properties removed from event.Job.
I struggled with hours on this, including relearning everything I believed I knew about cloning objects in Es6 using the spread operator.
Why was 'LINE ONE' mutating the input as well?
Spread operator does shallow cloning same as Object.assign()
Shallow-cloning (excluding prototype) or merging of objects is now
possible using a shorter syntax than Object.assign().
Spread operator
An example to understand spread operator and shallow cloning.
let obj = { 'a': { 'b' : 1 },'c': 2}
let copy = {...obj}
copy.c = 'changes only in copy' //shallow-cloned
copy.a.b = 'changed' // still reference
console.log('original\n',obj)
console.log('\ncopy',copy)
Using spread operator object is shallow cloned so all the first level properties will become a copy while all the deeper level properties will still remain the references.
so as you see in example c property doesn't affect the original object since it is one first level depth, on the other hand b property changes affect the parent properties because it is at deep level and is still reference.

Methods on an object created via composition can't access all properties

I'm building the concept of a family for a product, with members being of different types (accountHolder, payingCustomer, student, and so on). Originally I built these as sub-classes of FamilyMember, but I ended up with some repeated code and eventually bumped into a significant problem: a student of our platform can also be the sole payingCustomer and accountHolder.
Given how object composition is widely touted as a good idea in JS, I decided to go that route. However, the methods of a particular object type (e.g. accountHolder) can't access properties of the instantiated object, if the property belong to another object type (e.g. student).
To make this more objective I've decided to replicate the behaviour using the following code:
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = () => {
// state.isAdult is always undefined because
// isAdult doesn't exist in this object
return state.isAdult === true
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.clear()
console.log(john) // { isAdult: true, name: "John", isOfAge... }
console.log(john.isOfAge()) // false
I was expecting john's method isOfAge to be able to access the property isAdult, since it's in the object. However, conceptually I understand why it doesn't work: isOfAge is a method of state, not the resulting adult instance.
If I were using classes or even a traditional prototype/constructor mechanism I knew how to make it work (e.g. attaching to prototype). With object composition I've no idea how to get there, probably due to lacking experience with FP.
Thanks for the help!
You can use this instead of state inside isOfAge. That way, the this will be deduces when the method isOfAge gets called, it will be bound to whatever object it is called on. Though, you'll have to use a regular function instead of an arrow one for that to work (arrow functions don't have a this):
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = function() { // use a regular function
return this.isAdult === true // use this here instead of state
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.log(john);
console.log(john.isOfAge()); // returns 'true' because 'this' inside 'isOfAge' will be 'john'
Object Composition
All objects made from other objects and language primitives are composite objects.
The act of creating a composite object is known as composition.
...
Concatenation composes objects by extending an existing object with new properties, e.g., Object.assign(destination, a, b), {...a, ...b}.
...                      The Hidden Treasures of Object Composition
So from your pattern and use of a factory function it looks like concatenation? The demo below is a concatenation composition. Note the parenthesis wrapped around the brackets of payment:
const payment = (status) => ({...})
this allows payment to be returned as an object instead of a function. If you have data that's a little more flexible, you'll need less methods. name: string and age: number are the properties I used considering it practical or in your case name: string and adult: boolean.
Demo
const payment = (status) => ({
adult: () => status.age > 17 ? true : false,
account: () => status.adult() ? 'holder' : 'student'
});
const member = (name, age) => {
let status = {
name,
age
};
return Object.assign(status, payment(status));
};
const soze = member('Kaiser Soze', 57);
console.log(soze);
console.log(soze.adult());
console.log(soze.account());
const jr = member('Kaiser Soze Jr.', 13);
console.log(jr);
console.log(jr.adult());
console.log(jr.account());

Ngrx observables not reacting to state store change when using Object.assign() in combination with other operations

I have some trouble understanding why the first sample seems to work ok but the second one has trouble firing the observers.
// Working reducer
return Object.assign({}, state, {
expanded: Object.assign({}, state.expanded, { clients: !state.expanded.clients })
});
// Faulty reducer - Devtools indicates a change but the observables
// seem to not respond and pass on the information down the line
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
// Selectors
import {createSelector} from 'reselect';
export const SIDEBAR = (state: AppState) => state.sidebar;
export const SIDEBAR_UI = createSelector<AppState, SidebarUIState, SidebarState>(
SIDEBAR,
(state: SidebarState) => state.ui
);
// Sidebar service
public getSidebarUIExpandedObservable(): Observable<SidebarUIExpandedState> {
debug('Get sidebar UI expanded observable');
return this._store.select(SIDEBAR_UI_EXPANDED);
}
Instead of doing :
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
do
return Object.assign(
{},
state,
{
expanded: Object.assign(
{},
state.expanded,
{
clients: !state.expanded.clients
}
)
}
);
Trick :
When you'll have Typescript >= 2.1.4 you'll be able to do it like that
return {
...state,
{
expanded: {
...state.expanded,
{
clients: !state.expanded.clients
}
}
}
};
Reducers require pure functions, that make use of immutable objects.
Hence you cannot mutate the newState object, but you would have to create a new object that has definite new state upon creation.
Your first example fits the immutability principle, because Object.assign copies the matching propertie(s) of the object in the third parameter to a copy of the object in the second parameter, before assigning this to target object (first parameter). Thus, the original state has remained unaffected, and a new state is obtained in the target object.
In other words, no original object state has been changed by object.assign in memory and the return value delivers a ready to go (no further mutations required ) new state object.
more info on Object.assign(): https://developer.mozilla.org/nl/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
In the second case there is a new object created at first (immutability still valid), subsequently, the state is mutated by assigning the boolean, hence the immutability principle is broken, and reducers usually do not accept this.
With the following in a reducer:
newState = Object.assign({}, state);
newState.expanded.clients = !state.expanded.clients;
the content of state.expanded is being mutated.
That is newState.expanded and state.expanded refer to the same object and that object's client property is being mutated. Code looking at expanded will see that the object reference is unchanged and will assume that the content has not changed either, but you have mutated it by toggling the value of its clients property.

Categories

Resources