I have been trying to deep clone a dynamic object in typescript and all methods just return an empty {}
object for this specific scenario!!
Here is the type of object I am trying to deep clone
fullValues: { [key : string ] : Array<string> },
NOTE: fullValues
is passed to a react component and the below mentioned operations happen in this react component! fullValues
is NEVER directly mutated throughout the lifecycle of the program and it is initially a state in the parent component as shown below:
const facetValues: { [key: string ] : Array<string> } = {};
// Type => facetedData?: FacetCollectionType
if (facetedData) {
Object.entries(facetedData).forEach(([key, value]) => {
Object.defineProperty(facetValues, key, { value: [] as string[]});
});
}
const [ facets, setFacets ] = useState<{ [key: string ] : Array<string> }>(facetValues);
{facetedData &&
Object.keys(facetedData).length !== 0 ?
Object.entries(facetedData).map(([key, options]) => (
<DataTableFacetedFilter
key={key}
options={options}
mainKey={key}
title={key}
fullValues={facets}
setSelectedValues={setFacets}
/>
))
:
null
}
Random example of how this object can be structured:
{
status: [],
plan: [],
}
I tried the following methods for deepcloning:
Using lodash deepclone
console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(_cloneDeep(fullValues)); // outputs {}
Using JSON stringify method
console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(JSON.parse(JSON.stringify(fullValues))); // outputs {}
However if I do this
let fullValues: { [key : string ] : Array<string> } = { status: [], plan: [] };
console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(_cloneDeep(fullValues)); // outputs { status: [], plan: [] }
It works here.
There seems to be no logic to why this is happening? It makes no sense!
2
Answers
This stems from the use of
Object.defineProperty
to set the fields.You can reduce it to this simplified example.
According to the MDN documentation for
defineProperty
:By being non-enumerable, these methods for cloning the object do not see these properties. Assuming that
defineProperty
was used deliberately to make the properties non-writable, you can make them explicitly enumerable:If making the properties non-writable and non-configurable isn’t required, a simpler solution is to use an indexed assignment:
Between the the snippet that works and your lodash snippet, the only difference is that you’re explicitly assigning a value to
fullValues
immediately before trying to deep clone it. That leads me to believe thatfullValues
is just empty (can you double-check that the console log for lodash and JSON.stringify snippets do actually print the object out with each render?).If the parent component is supposed to reset and re-populate
facetValues
with each render, then this might be an issue of a race/async condition in state setting during the parent’s lifecycle: Parent loads ->facetValues
= {} ->facets
==facetValues
->facetedData
may or may not be updated -> iffacetedData
is defined,facetValues
gets updated BUT there is no listener to updatefacets
after it was set to{}
. Also, note thatfacetedData
is an object, so theif
block will be entered even if it was set to{}
.Instead of the current parent logic, try using
useEffect
to listen to changes infacetedData
and call a callback (with afacetedData
dependency) to set the state offacets
, e.g.Edit: good catch by Tim. I updated the code.