Bastek Bastek
Bastek Bastek

Reputation: 29

Issue with state update approach for nested objects

Major EDIT

I have quite huge object which is 3 level deep. I use it as a template to generate components on the page and to store the values which later are utilized, eg:

obj =
 { 
  "group": {
    "subgroup1": {
      "value": {
        "type": "c",
        "values": []
      },
      "fields_information": {
        "component_type": "table",
        "table_headers": [
          "label",
          "size"
        ],
      }
    },
    "subgroup2": {
      "value": {
        "type": "c",
        "values": []
      },
      "fields_information": {
        "component_type": "table",
        "table_headers": [
          "label",
          "size"
        ],
      }
    },
  },
 }

Thanks to this I can dynamically generate view which is, as a template, stored in DB.

I'm struggling with 2 things. Firstly, updating values basing on user input for textbox, checkboxes and similar. I'm doing it this way:

    const updateObj = (group, subgroup, value) => {
        let tempObj = {...obj}
        tempObj[group][subgroup].value.value = value
        toggleObj(tempObj)
    }

I know that the spread operator is not in fact doing deep copy. However it allows me to work on the object and save it later. Is that an issue? Do I have to cloneDeep or it is just fine? Could cloneDeep impact performance?

Second case is described below

export const ObjectContext = React.createContext({
    obj: {},
    toggleObj: () => {},
});

export const Parent = (props) => {
    const [obj, toggleObj] = useState()
    const value = {obj, toggleObj}

    return (
        <FormCreator />
    )
}

const FormCreator = ({ catalog }) => {
    const {obj, toggleObj} = React.useContext(ObjectContext)

    return (<>
        {Object.keys(obj).map((sectionName, sectionIdx) => {
        const objFieldsInformation = sectionContent[keyName].fields_information
        const objValue = sectionContent[keyName].value
        ...
            if (objFieldsInformation.component_type === 'table') {
            return (
                <CustomTable 
                key={keyName + "id"}
                label={objFieldsInformation.label}
                headers={objFieldsInformation.table_headers}
                suggestedValues={[{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}
                values={objValue.values}
                sectionName={sectionName}
                keyName={keyName}/>
            )
            }
        ...
        })}
    </>)
}

const CustomTable= (props) => {
    const { label = "", headers = [], suggestedValues = [], values, readOnly = false, sectionName, keyName } = props
    const {obj, toggleObj} = React.useContext(ObjectContext) 
    
    //this one WORKS
    useEffect(() => {
        if (obj[sectionName][keyName].value.type === "complex") {
            let temp = {...obj}
            temp[sectionName][keyName].value.values = [...suggestedValues]
            toggleObj(temp)
        }
    }, [])
    
    //this one DOES NOT
    useEffect(() => {

        if (obj[sectionName][keyName].value.type === "c") {
            let temp = {...obj, [sectionName]: {...obj[sectionName], [keyName]: {...obj[sectionName][keyName], value: {...obj[sectionName][keyName].value, values: [{label: "", size: ""}, {label: "", size: ""}, {label: "", size: ""}]}}}}
            toggleObj(temp)
        }
    }, [])
    
    return (
    //draw the array
    )
}

Please refer to CustomTable component. As on the example Object above, I have 2 CustomTables to be printed. Unfortunately, one useEffect that should work is not working properly. I'm observing, that values field is set only for the last "table" in Obj. When I'm doing shallow copy of obj, it works fine. But I'm afraid of any repercussion that might happens in future.

I'm also totally new to using createContext and maybe somehow it is the issue.

Kudos to anyone understanding that chaos :)

Upvotes: 0

Views: 816

Answers (1)

lawrence-witt
lawrence-witt

Reputation: 9354

The main issue appears to be that you are not providing your context. What you have is literally passing the blank object and void returning function. Hence why calling it has no actual effect, but mutating the value does.

export const ObjectContext = React.createContext({
    obj: {},
    toggleObj: () => {},
});

export const Parent = (props) => {
    const [obj, toggleObj] = useState({})
    const value = {obj, toggleObj}

    return (
        <ObjectContext.Provider value={value}>
           <FormCreator />
        </ObjectContext.Provider>
    )
}

Ideally you would also make this component above wrap around FormCreator and render it as props.children instead. This is to prevent the entire sub-tree being rerendered every time toggleObj is called. See the first part of this tutorial to get an idea of the typical pattern.

As to the question about mutating state, it absolutely is important to keep state immutable in React - at least, if you are using useState or some kind of reducer. Bugs arising from state mutation come up all the time on Stack Overflow, so often in fact that I recently made a codesandbox which demonstrates some of the more common ones.

I also agree with @SamuliHakoniemi that a deeply nested object like this is actually better suited to the useReducer hook, and might even go one further and suggest that a proper state management library like Redux is needed here. It will allow you to subdivide reducers to target the fragments of state which actually update, which will help with the performance cost of deeply cloning state structure if or when it becomes an actual issue.

Upvotes: 1

Related Questions