-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
react update - Cannot update a component from inside the function body of a different component. #128
Comments
Thanks for the issue @keeganstothert - worth mentioning that there are a lot of similar issues being reported on other libraries as well since the release of React 16.13.0. It has to do with setting state synchronously during a render phase. I'm not entirely sure if this will be an easy/quick fix. Please feel free to investigate or submit a PR 👍 |
I have the same error when tried to make a wizard form |
Same issue when creating reusable components where formState is set in a parent component but the form fields that update formState are in child components (the function body of a different component). |
Yep, I can verify that I've seen this same behavior in my app. Required some restructuring to get around it. Pretty sure the issue is this logic here: react-use-form-state/src/useFormState.js Lines 206 to 223 in 16df900
When React does technically allow state updates to be queued in function components, but it's really only meant for the use case of deriving state from props. Internally, React will throw away the first render result for the component and immediately try again with the updated state. If we were to measure what's happening in the component, I'm pretty sure we'd see that it's actually executing the function component multiple times because of this behavior. Since React absolutely forbids triggering updates in other components while rendering, this logic breaks as soon as you try passing the input handlers to a child component and using them there. |
Hi folks, (and thanks for chiming in @markerikson!) Here's my two cents. I really hope this helps! :) What's happening?It is indeed Adding to what @markerikson said, Because of that const Child = ({ setValue }) => {
setParentValue('test');
return null;
};
const Parent = () => {
const [value, setValue] = useState('');
return <Child setValue={setValue} />;
}; Can this be fixed?This is not an easy fix. To truly avoid this error, What can be done?Luckly, you can still avoid this error. After all, It's not the fact that you're passing the To better illustrate this, consider the following example: 𝖷 Bad example (will cause an error) const FancyInput = ({ inputProps }) => {
// `inputProps.value` is being accessed from the child component
return <input {...inputProps} />;
}
const Form = () => {
const [value, { text }] = useFormState();
return <FancyInput inputProps={text('username')} />;
}; ✓ Working example (1): spread the input props instead of passing them as a single prop. Exactly as this library intended. const FancyInput = ({ onChange, value }) => {
return <input value={value} onChange={onChange} />;
}
const Form = () => {
const [value, { text }] = useFormState();
// `.value` is accessed and evaluated from the parent component before
// its return value is passed down
return <FancyInput {...text('username')} />;
}; ✓ Working example (2): Spread the input props over a new object (although it's discouraged for expensive components since you're passing new objects on every render). This is very similar to the first bad example above with a single object prop being passed, but it no longer causes the error because the const Form = () => {
const [value, { text }] = useFormState();
- return <FancyInput inputProps={text('username')} />;
+ return <FancyInput inputProps={{ ...text('username') }} />;
};
@markerikson below is my response to your comment but it's collapsed as it's slightly off-topic to the original issue: While this behavior might give the impression that the parent component will re-render every time Additionally, with React batching state updates, there's no coloration between the number of subsequent renders caused by field initializations and the number of fields used in a form. const FancyInput = ({ value, onChange, name }) => {
console.count(name);
return <input value={value} onChange={onChange} />;
};
const Form = () => {
const [form, { text }] = useFormState();
console.count('form');
useEffect(() => console.log('form is rendered'));
return (
<form>
<FancyInput {...text('foo')} />
<FancyInput {...text('bar')} />
<FancyInput {...text('baz')} />
</form>
);
};
// => form: 1
// => form: 2
// => foo: 1
// => bar: 1
// => baz: 1
// => form is rendered |
@wsmd : thanks for the detailed technical explanation! In my specific case, we've got a parent component that shows one child form at a time, but the specific form it's showing gets switched as the user changes categories. However, all the "Save / Cancel / Reset" buttons live in the parent component, so the form state has to live there as well. Each child file supplies its own unique initial form state and logic for rendering its form UI, and the parent doesn't actually know what fields are in the form. Hmm. Given what you've described, would this be a viable workaround? const {
initialFormState,
childFormComponent : FormComponent
} = childFormDefinitions[category];
const [formState, {raw}] = useFormState(initialFormState);
// HACK WORKAROUND
Object.keys(initialFormState).forEach(key => {
const fieldProps = raw(key);
// Force registration of field with useFormState
const dummy = fieldProps.value;
});
// later
return <ChildForm formState={formState} raw={raw} /> and then the child form can do whatever it needs to with those. |
Thank you for the detailed explanation @wsmd, but I'm not sure how viable the proposed solution is as this would require the parent to be aware of and initialize every form field for all of its children, and when you start building complex forms with many fields this quickly becomes unmanageable... Similar to @markerikson's structure above, I'd really like to just be able to pass
Is there any way to do this without violating the "Cannot update a component from inside the function body of a different component" rule? |
So rather than passing input props (
becomes:
clearly more manual and verbose, but it prevents the "Cannot update" warning and doesn't require the parent form to register/be aware of all its fields |
But that kinda defeats the purpose of this library in the first place :( Much of the selling point is "being able to spread props for this field on an input". |
Another way to work around this, is to create a compound component for your form template (rather than a 'god' Form component). So rather than this, which results in the React error: <Form
title="Update module details"
fields={[
{
type: 'input',
label: 'Title',
labelProps: label('title'),
inputProps: text('title'),
},
{
type: 'input',
label: 'Slug',
labelProps: label('slug'),
inputProps: {
...text('slug'),
onBlur: () => form.setField('slug', slug(form.values.slug)),
},
prefix: module.path.slug + '/',
},
]}
onSubmit={submit}
errors={Object.values(form.errors)}
/> You do something like this: <Form onSubmit={submit}>
<Form.Header>Update module details</Form.Header>
<Form.Body>
<Form.Field label={<label {...label('title')}>Title</label>}>
<input {...text('title')} />
</Form.Field>
<Form.Field
label={<label {...label('slug')}>Title</label>}
prefix={`/${module.path.slug}`}
>
<input
{...text('slug')}
onBlur={() => form.setField('slug', slug(form.values.slug))}
/>
</Form.Field>
</Form.Body>
<Form.Footer errors={Object.values(form.errors)} />
</Form> This solves the issue, is easier to type and in my opinion is even cleaner as well. A compound component also gives more flexibility; you can easily change form layout or swap the header for a different one in a certain form for instance. |
https://github.com/facebook/react/issues/18178
my strack trace links the above issue with this code in react-use-form-state:
I'm still digging into this because
Cannot update a component from inside the function body of a different component.
has popped up everywhere in my app.The text was updated successfully, but these errors were encountered: