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!
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!
This stems from the use of Object.defineProperty
to set the fields.
You can reduce it to this simplified example.
const values : { [key: string]: Array<string> } = {};
console.log(`1: ${JSON.stringify(values)}`); // 1: {}
Object.defineProperty(values, 'status', { value: 'ok' });
console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
console.log(`3: ${JSON.stringify(values)}`); // 3: {}
According to the MDN documentation for defineProperty
:
By default, properties added using Object.defineProperty() are not writable, not enumerable, and not configurable.
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:
const values : { [key: string]: Array<string> } = {};
console.log(`1: ${JSON.stringify(values)}`); // 1: {}
Object.defineProperty(values, 'status', { value: 'ok', enumerable: true });
console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
console.log(`3: ${JSON.stringify(values)}`); // 3: {"status":"ok"}
If making the properties non-writable and non-configurable isn't required, a simpler solution is to use an indexed assignment:
const values : { [key: string]: Array<string> } = {};
console.log(`1: ${JSON.stringify(values)}`); // 1: {}
const key = 'status';
values[key] = 'ok'; // `values.status = 'ok'` would also work for non-variable keys
console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
console.log(`3: ${JSON.stringify(values)}`); // 3: {"status":"ok"}
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 that fullValues
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-populatefacetValues
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 -> if facetedData
is defined, facetValues
gets updated BUT there is no listener to update facets
after it was set to {}
. Also, note that facetedData
is an object, so the if
block will be entered even if it was set to {}
.
Instead of the current parent logic, try using useEffect
to listen to changes in facetedData
and call a callback (with a facetedData
dependency) to set the state of facets
, e.g.
const [ facets, setFacets ] = useState<{ [key: string ] : Array<string> | null }>(null);
const mapFacets = useCallback(() => {
if (!facetedData) return;
const facetValues: { [key: string ] : Array<string> } = {};
Object.entries(facetedData).forEach(([key, value]) => {
facetValues[key] = value as string[]
});
setFacets(facetValues)
}, [facetedData]);
useEffect(() => {
mapFacets();
}, [facetedData]);
{facets ?
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
}
Edit: good catch by Tim. I updated the code.