Reputation: 1473
I have a complex form in React containing a dozen sub-components. It also includes my own form components that operate by not single values. Some parts of the form use trees of objects as data.
I have the main component (let's call it EditForm) which stores the whole object tree of data. I assume each component of this form should use a specific part of this main object. I should have the ability to store whole form data into JSON and read it back from JSON (and update all sub-components automatically when JSON changed).
The idea was to refresh only those parts of the form that required when I change the main JSON. But the code which handles this behavior is dramatically big and complex. I have to implement comparison functionality into each sub-component and also transfer parts of the main JSON into props of each sub-component.
const [testobj, setTestobj] = useState({'obj1': {......}, 'obj2': {.......}, .......});
function revalidateSub(sub_name, value)
{
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
setTestobj(t2);
}
return (<MySubComponent subobj={testobj['sub1']} ident={"sub1"} onUpdateData={v => revalidateSub('sub1', v)}/>)
(subobj
is the piece of data which is controlled by a specific sub-component, onUpdateData
is a handler which is called inside the subcomponent each time when it makes any changes to a controlled piece of data)
This scheme, however, does not work as expected. Because revalidateSub() function stores its own copy of testobj and onUpdateData of each component rewriting the changes of other sub-components...
To be honest, I don't quite understand how to store data correctly for this type of React apps. What do you propose? Maybe I should move the data storage out of React components at all? Or maybe useMemo?
Thank you for the shared experience and examples!
Update1: I have coded the example in the Sandbox, please check https://codesandbox.io/s/thirsty-browser-xf4u0
To see the problem, please click some times to the "Click Me!" button in object 1, you can see the value of obj1 is changing as well as the value of the main object is changing (which is CORRECT). Then click one time to the "Click Me!" of the obj2. You will see (BOOM!) the value of obj1 is reset to its default state. Which is NOT correct.
Upvotes: 2
Views: 768
Reputation: 221
First of all, I think the idea is correct. In React components, when the value of the props passed from the state or the parent component changes, re-rendering should be performed. Recognized as a value.
Because of this issue, when we need to update a multilayered object or array, we have to use ... to create a new object or array with an existing value. It seems to be wrong during the copying process.
I don't have the full code, so a detailed answer is not possible, but I think the bottom part is the problem.
let t2 = cloneDeep(testobj);
There are many other solutions, but trying Immutable.js first will help you find the cause of the problem.
Consider also useReducer.
update
export default function EditForm(props) {
const [testobj, setTestobj] = useState({
sub1: { id: 5, v: 55 },
sub2: { id: 7, v: 777 },
sub3: { id: 9, v: 109 }
});
function revalidateSub(sub_name, value) {
let t2 = cloneDeep(testobj);
t2[sub_name] = value;
return t2;
}
console.log("NEW EditForm instance was created");
function handleTestClick() {
let t2 = cloneDeep(testobj);
t2.sub2.v++;
setTestobj(t2);
}
return (
<div>
<div style={{ textAlign: "left" }}>
<Button onClick={handleTestClick} variant="contained" color="secondary">
Click Me to change from main object
</Button>
<p>
Global value:{" "}
<span style={{ color: "blue" }}>{JSON.stringify(testobj)}</span>
</p>
<MyTestObj
subobj={testobj["sub1"]}
ident={"sub1"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub1", v))}
/>
<MyTestObj
subobj={testobj["sub2"]}
ident={"sub2"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub2", v))}
/>
<MyTestObj
subobj={testobj["sub3"]}
ident={"sub3"}
onUpdateData={(v) => setTestobj((p) => revalidateSub("sub3", v))}
/>
</div>
</div>
);
}
The reason it bursts when you do the above setState updates the local state asynchronously.
The setState method should be thought of as a single request, not an immediate, synchronous execution. In other words, even if the state is changed through setState, the changed state is not applied immediately after the method is executed.
Thus, your code
setTestobj((p) => revalidateSub(p, "sub3", v))
It is safer to use the first parameter of setstate to get the full state like this!
Thanks to this, I was able to gain new knowledge. Thank you.
Upvotes: 0
Reputation: 1473
I think I have solved this by myself. It turned out, that the setState function may also receive the FUNCTION as a parameter. This function should return a new "state" value.
I have moved revalidateSub()
function OUT from the EditForm component, so it does not duplicate the state variable "testobj
" anymore. And it is now working as expected.
Upvotes: 1