0

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 :)

  • 1
    Your question is not clear - for a start, `subGroup` is an array so `[subGroup].value` should always be undefined. But your last two examples are also almost identical except that the second is mutative (and you access `values` vs `items`), so I don't know what you mean when you say one works but the other does not. Needlessly mutating state is inevitably going to lead to silent errors later on down the line - if you have an effect or memoised component which is dependent on `subGroup` for instance, it will not be run when that group's `value.values` is mutated. – lawrence-witt Jan 26 '21 at 22:34
  • @lawrence-witt I just put it as an reference example. I do not have any issue with setting undefined and so on (examples provided may be messed up). May I refer firstly to "inevitably going to lead to silent errors". Does it? I'm making a "copy" of object defined with const [obj, toggleObj] = useState(), I'm updating tempObj (which is also updating obj) but I update the state with toggleObj which in fact is overwriting obj. So does the mutation of obj couple moments ago matter? – Bastek Bastek Jan 27 '21 at 07:34
  • Furthermore, about 2 types of update I did (which the later one works) is my main concern. I was assigning correctly mutated obj to var before triggering toggleObj and printed it before and looked as desired. However, in grandparent, when I tried to monitor changes on desired object with useEffect(()=>{}) nothing really happened. I wanted to render 3 tables that way, and rows were only visible for the last one. I think it's a little to complicated to describe it. I will try to make a correct example with all components. – Bastek Bastek Jan 27 '21 at 07:44
  • 1
    Just a side-note, but I personally prefer using _useReducer_ over _useState_ when the state structure has more complexity / depth. – Samuli Hakoniemi Jan 27 '21 at 08:33

1 Answers1

1

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.

lawrence-witt
  • 8,094
  • 3
  • 13
  • 32
  • Thank you for your suggestion! I rewrote updates as suggested in tutorial to setState(state => ({...state,....) and mutating the state seems resolved. However, I would like to ask you about some details regarding "Ideally you would...". I did FormCreatorProvider as separate component: ` {props.children} ` and wrapped FormCreator withing this provider like: ` ` – Bastek Bastek Jan 27 '21 at 21:32
  • and now the issue is, that everything in FormCreator is rerendered (it was also before btw). Could this be avoided? Furthermore, I'm actually using redux with thunk middleware for all async call (for example to get mentioned template). Contexts were just something new to test. Anyway, with redux, should I create separate store for those kind of actions? Furthermore, this object I'm rendering is quite dynamic. I'm wondering how to prepare actioners and reducers when fields differs for each object. – Bastek Bastek Jan 27 '21 at 21:38
  • Sorry, I should have specified, any component consuming the context will be rerendered. The benefit of the `props.children` pattern is that you can cover more of your sibling components with context without rerendering them unless they consume it. If you're only using it in this one place to avoid prop drilling then its not as relevant. Redux is another huge topic in itself and probably beyond the scope of this question - [starting here](https://redux.js.org/recipes/structuring-reducers/basic-reducer-structure) and reading the whole section on structuring reducers would be a good start! – lawrence-witt Jan 27 '21 at 22:24