Skip to content
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

FormSpy - Cannot update a component while rendering a different component #809

Open
ThaNarie opened this issue Jun 15, 2020 · 17 comments · May be fixed by #965
Open

FormSpy - Cannot update a component while rendering a different component #809

ThaNarie opened this issue Jun 15, 2020 · 17 comments · May be fixed by #965

Comments

@ThaNarie
Copy link

Are you submitting a bug report or a feature request?

bug report

What is the current behavior?

When using FormSpy, the onChange is called during rendering, and throws an error if you use setState.

<FormSpy
  subscription={{ values: true }}
  onChange={change => {
    // fired during rendering, calling a `useState` setter fails
    setValues(change.values);
  }}
/>
Warning: Cannot update a component (`App`) while rendering a different component (`FormSpy`). To locate the bad setState() call inside `FormSpy`, follow the stack trace as described in https://fb.me/setstate-in-render
    in FormSpy (at App.js:23)
    in form (at App.js:13)
    in ReactFinalForm (at App.js:10)
    in App (at src/index.js:9)
    in StrictMode (at src/index.js:8)

What is the expected behavior?

No error.

Sandbox Link

https://codesandbox.io/s/react-final-form-formspy-hltnv?file=/src/App.js

What's your environment?

    "final-form": "4.20.0",
    "react": "16.13.1",
    "react-dom": "16.13.1",
    "react-final-form": "6.5.0",

Other information

The issue seems to be that onChange in the useFormState is called in the useState init function, which means during rendering:

onChange(initialState)

This is part of a range of error messages introduced in react 16.13, and related to an existing issue (#751)

A workaround would be to put any setState on the onChange in a setTimeout(... ,0), but this feels like a hack, and not clear to new users.

@kopax
Copy link

kopax commented Jul 6, 2020

I am trying to read outside of the form the state saved in Redux and I got the same warning :

Related issue: #828

Is there a workaround or am I stuck with it?

@muyiwaoyeniyi
Copy link

@erikras Please any help on this? While the changes you published to #751 seem to work, I don't think it covers setting state inside the onChange function in a FormSpy.

Thanks.

@szankrisz
Copy link

The issue still persists with react-final-form@6.5.1 and final-form@4.20.1 with react version 16.13.1. Here's what I've found after digging through the (compiled) library source:

  1. useField() is invoked internally by the react-final-form library from within the Field component.
  2. It registers the field from within the lazy initializer function of a useState() hook. This parametrizes the internal register() function with a true second argument which is the silent flag. All is fine at this point.
  3. However, despite the silent setting, final-form goes into a validation loop. I see in the stack trace that it invokes runValidation() which in turn leads to notifyFieldListeners() and then to notifySubscribers() on each field. This actually brings the code path back to react-final-form, into the useEffect() call inside of which the field is registered again but this time without the silent flag.
  4. The actual culprit is:
    React.useEffect(function () {
      return register(function (state) {
        if (firstRender.current) {
          firstRender.current = false;
        } else {
          setState(state);
        }
      }, false);
    }

I think the following happens: from the aspect of one particular field, registration indeed happens properly (first with the silent and then without the silent flag) but other fields could have completed this cycle already. So, when a field gets initialized it runs into the validation loop, causing the setState() call to take place inside other fields, thereby violating the rule.

I have no suggestion at this point as to how it could be solved though.

@floriangosse
Copy link

Does someone already have a solution or workaround for that problem?

@ignatevdev
Copy link

In my case this error was caused by a mutator, which was called on every FormSpy render.

I've solved this problem by wrapping the mutator call inside a useLayoutEffect and now everything works like before.

@cordial
Copy link

cordial commented Mar 22, 2021

Does anyone have an example solution to this?

@theZappr
Copy link

Hi all, I have been battling this issue while migrating from Redux-Form to React-Final-Form. Redux-Form has isDirty() in its API so I could easily get the state of a Form and use it in another (external) component. The only similar thing I can find for Final Form is FormSpy ( if not please let me know! ). In the end I had to give up and push the "dirty" flag into redux (in effect, rolling my own isDirty() for final-form) : which is when I bumped into the issue you are talking about here.

So far the only way I can get rid of the Warning is by wrapping the redux dispatch call in a cheeky setTimeout(,0). Hacky, stochastic, but it is working at least. Defo not a solution long term but thought I'd share :)

   // passed to FormSpy's onChange
   onChange = ({dirty}) => {
    // dodgy workaround to avoid Warning :P
    setTimeout(()=>this.props.dispatch( formActions.changed({dirty}) ) ,0)
  }

@igorpupkinable
Copy link

I am experiencing the same issue in our project.
I can help with investigation and report error details if needed.

@mzehir
Copy link

mzehir commented Nov 4, 2021

@ThaNarie I hope you get rid of the error when you write it like this. this is how i solved it
<FormSpy
subscription={{ values: true,modified: true, }}
onChange={change => {
// fired during rendering, calling a useState setter fails
if(Object.keys(state.modified).length > 0){
setValues(change.values);}
}}
/>

@osya
Copy link

osya commented Jan 24, 2022

In my case this error was caused by a mutator, which was called on every FormSpy render.

I've solved this problem by wrapping the mutator call inside a useLayoutEffect and now everything works like before.

Could you please give an example?

@vitbokisch
Copy link

Seems like one of the solutions can be creating your own Spy component in such a case, e.g. simple implementation could look like this:

import { VFC, useEffect } from 'react'
import { useFormState } from 'react-final-form'

type Props = {
  onChange: (props: any) => void
}

const component: VFC<Props> = ({ onChange }) => {
  const state = useFormState()

  useEffect(() => {
    onChange(state)
  })

  return null
}

export default component

@KienHui
Copy link

KienHui commented Apr 8, 2022

Little react hook: don't have a real onChange handler until after the first render is called.

function useAfterFirstRender(func) {
  const [retFunc, setFunct] = useState(() => ()=>{});
  useLayoutEffect(() => {
    setFunct(() => func);
  }, []);
  return retFunc;
}

@fbolcic
Copy link

fbolcic commented Aug 1, 2022

Hi guys,
Is there any fix for this issue on the horizon? We are experiencing the same error; Although, our stack involves slightly more moving parts so it is difficult to diagnose correctly where the issue arises:
react-final-form: ˆ6.5.7
react-final-form-arrays: ˆ3.1.3
@dnd-kit/sortable: 7.0.0
Also various ui component libraries.

As far as I understood, the issue is caused by final-form setting state while rerendering?
Are there any news from the lib maintainer?

@gertdreyer
Copy link
Collaborator

Please see #985 (comment)

@NikolaDojic
Copy link

NikolaDojic commented May 24, 2023

Please see #985 (comment)

That is a different issue

GrishaAngelovGH added a commit to GrishaAngelovGH/grocery-store that referenced this issue Jan 26, 2024
…nal-form library.

final-form/react-final-form#809

Due to inconsistent behavior from react-final-form resulting in the provision of old values,
the Shipping component will apply the selected shipping method on mount.

The call of handleShippingMethodChange(), which was previously used by FormSpy,
will be performed when we click on the given radio button.
GrishaAngelovGH added a commit to GrishaAngelovGH/grocery-store that referenced this issue Jan 26, 2024
…nal-form library.

final-form/react-final-form#809

Due to inconsistent behavior from react-final-form resulting in the provision of old values,
the Shipping component will apply the selected shipping method on mount.

The call of handleShippingMethodChange(), which was previously used by FormSpy,
will be performed when we click on the given radio button.
GrishaAngelovGH added a commit to GrishaAngelovGH/grocery-store that referenced this issue Jan 26, 2024
…nal-form library.

final-form/react-final-form#809

Due to inconsistent behavior from react-final-form resulting in the provision of old values,
the Shipping component will apply the selected shipping method on mount.

The call of handleShippingMethodChange(), which was previously used by FormSpy,
will be performed when we click on the given radio button.
@hurrii
Copy link

hurrii commented Mar 12, 2024

wrapping the callback in requestAnimationFrame solved the problem for me

<FormSpy
    subscription={{ values: true }}
    onChange={(state) => {
        requestAnimationFrame(() => {
             // your callback body
        });
    }}
/>

@stec00
Copy link

stec00 commented Mar 27, 2024

For me, creating a new component that only fires the event if the page has rendered did the job:

import { useEffect, useRef } from "react";
import { FormSpy } from "react-final-form";

export const FormSpyWhenReady = ({
  subscription,
  onChange,
}: {
  subscription: { values: boolean };
  onChange: () => void;
}) => {
  const readyRef = useRef(false);
  useEffect(() => {
    readyRef.current = true;
  }, []);

  return (
    <FormSpy
      subscription={subscription}
      onChange={() => {
        if (readyRef.current) {
          onChange();
        }
      }}
    />
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.