wsmd / react-use-form-state

πŸ“„ React hook for managing forms and inputs state

Home Page:http://react-use-form-state.now.sh

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

react update - Cannot update a component from inside the function body of a different component.

keeganstothert opened this issue Β· comments

https://github.com/facebook/react/issues/18178

my strack trace links the above issue with this code in react-use-form-state:

if (formState.current.touched[name] == null) {
 >>>  formState.setTouched(_defineProperty({}, name, false));
}

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.

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:

get value() {
if (!hasValueInState) {
// auto populating default values if an initial value is not provided
setDefaultValue();
} else if (!formState.initialValues.has(name)) {
// keep track of user-provided initial values on first render
formState.initialValues.set(name, formState.current.values[name]);
}
// auto populating default values of touched
if (formState.current.touched[name] == null) {
formState.setTouched({ [name]: false });
}
// auto populating default values of pristine
if (formState.current.pristine[name] == null) {
formState.setPristine({ [name]: true });
}

When inputProps.value gets read in a component as part of rendering, it actually triggers a state update. This is not correct React behavior.

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 inputProps.value what could potentially cause the warning when it's accessed from another component/function.

Adding to what @markerikson said,inputProps.value not only retrieves the input value, but also acts as an implicit register/initializer function. It basically tells the hook: "hey, the field exists now, so keep track of it" (as you can tell from the snippet above). This behavior is what makes this hook's approach a bit unique because you don't have to explicitly define the fields ahead of time. Instead, initialization happens automatically during the render phase based on what fields you actually render.

Because of that inputProps.value must be accessed and evaluated from within the parent component, otherwise, React will complain. Simply put, this is what's happening when .value is accessed from a children component:

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, useFormState would have to either (A) know when a child component has rendered so initialization would have to occur during an effect inside the child, or (B) change how fields are initialized, which is the whole point of this hook πŸ˜… . There might be other tricks that I'm not thinking about at the moment...

What can be done?

Luckly, you can still avoid this error.

After all, It's not the fact that you're passing the .value prop down to the child component. It's the where the .value prop being accessed is what matters! As long as you make sure .value is evaluated from inside the parent component, there shouldn't be any problems.

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 .value prop is access and evaluated within the parent component as the object is being iterated over.

 const Form = () => {
   const [value, { text }] = useFormState();
-  return <FancyInput inputProps={text('username')} />;
+  return <FancyInput inputProps={{ ...text('username') }} />;
 };

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.

@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 inputProps.value is called, it's not as bad as it sounds. A form consuming useFormState will render as follows: initial render (mounting the component tree), followed by another immediate render caused by the form state fields being initialized during the first render (access to inputProps.value). Children components will render only once despite the immediate re-render of the parent component.

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 raw, text, etc., down to children of the form and let them initialize their fields as needed, i.e.:

const SomeParentForm = () => {
  const [formState, { raw, text }] = useFormState();

  return (
    <div>
      <SomeFormSection
        rawProps={raw}
        textProps={text}
      />
      <AnotherFormSection
        textProps={text}
      />
    </div>
  )
};

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 (text, radio, raw, etc.) down, you can pass down the pieces you need for the child fields to manipulate and represent the form (setField, values, errors, etc.), so:

const SomeChildFormSection ({ radio }) => {
  // ...

  return (
    // ...
      <input {...radio('someField')} />
    // ...
  )
}

becomes:

const SomeChildFormSection ({ values, setField }) => {
  // ...

  return (
    // ...
      <input
        type="radio"
        checked={values.someField}
        onChange={e => setField('someField', !e.target.checked)}
      />
    // ...
  )
}

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.