skip to Main Content

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


  1. 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"}
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search