jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.87k stars 2.79k forks source link

helpers.setValue causes infinite loop when called inside useEffect in React 18 #3602

Open antonvialibri1 opened 2 years ago

antonvialibri1 commented 2 years ago

Bug report

Calling helpers.setValue results in an infinite loop when it's called inside useEffect in React 18.

React 17: https://codesandbox.io/s/formik-usefield-hook-react-18-bug-forked-ucpg4d?file=/src/App.js

React 18: https://codesandbox.io/s/formik-usefield-hook-react-18-bug-nr052g?file=/src/App.js

The code of the App component is exactly the same on both sandboxes, what changes is the React major version.

Expected behavior

React 17: If you click on the CLICK button in the React 17 sandbox, you will see that the input value will change from aaa@mail.com to bbb@mail.com 🆗 ✅

Current Behavior

React 18: If you click on the CLICK button in the React 18 sandbox, it will cause an infinite loop ⚠️❗

Suggested solution(s)

I don't know exactly what could be the root cause of this issue, but I suspect that it's related to the Automatic Batching feature/breaking change introduced by React 18:

https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching

What happens in practice is that the reducer for SET_FIELD_VALUE is never called for some reason, even though it's action gets dispatched within the setFieldValue function. The reducer for SET_ISVALIDATING is executed instead.

On React 17:

On React 18:

The infinite loop is then caused by the useField hook always returning the old value instead of the new value that was set through helpers.setValue(value).

Screenshot 2022-07-11 at 17 23 50

Screenshot 2022-07-11 at 17 23 33

Screenshot 2022-07-11 at 17 24 45

Your environment

Software Version(s)
Formik 2.2.9
React 18.2.0
TypeScript -
Browser Chrome
npm/Yarn Yarn
Operating System Mac OS X
LahmerIlyas commented 1 year ago

Is there a workaround for this?

marcoromag commented 1 year ago

I found a workaround to this subtle bug by wrapping the setValue call in a ReactDOM.flushSync. In this way, the operation will not be batched and we get a similar behaviour as of React 17

  const [{ value }, , { setValue }] = useField(name);
  useEffect(() => {
    if (value === null)
      flushSync(() => setValue("now it is not null"));
  }, [value, fields, setValue]);
antonvialibri1 commented 1 year ago

@marcoromag Thanks for sharing! It would be good that this issue is addressed by the Formik maintainers though.

matths commented 1 year ago

@marcoromag I get Warning: flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.with the workaround. How about you?

matths commented 1 year ago

I downgraded to React 17 just to ensure myself this is really related to. And it is! Nonetheless because Formik encapsulates its state handling, Formik users can't do much about this without the help of Formik maintainers.

danhooper commented 1 year ago

For what it's worth I fixed this by wrapping the call to formik.setFieldValue in a setTimeout to avoid the flushSync warning that @matths mentioned.

matths commented 1 year ago

Sorry, but when you need to trick a framework or library using setTimeout, well, I would call this a hack. In my opinion, Formik is still heavily used and there are still people starting to use it right now, but they should not or they should be aware that they will go with a fork to keep this up to date with future React. At least for my project, we switched to React Hook Form, which is, as of now, a very active project, with fewer open issues or pull requests. It would be more then fair, when @jaredpalmer would put a disclaimer into the README that there's no active development right now. Maybe I am wrong, but the last release is from June 2021.

fastndead commented 1 year ago

Hi! Are there any updates regarding this issue? I've ran into this issue 2.2.6 and it seems to be relevant on the latest patch too.

Also, to add to the workaround conversation, it seems to me that this issue only affects setValue of useField hook, but doesn't affect useFormikContext's setFieldValue. I managed to refactor code to use setFieldValue and everything works as expected.

In any case this should be noted for anyone trying to upgrade to React 18, that this kind of issue can happen and it can hang the whole website. Pretty dangerous

brenoprata10 commented 1 year ago

I am having the same issue, using useFormik like this

const formik = useFormik()
useEffect(() => {
      formik.setFieldValue('field', value)
}, [formik])

Using setFieldValue like this removes the infinite loop

const {setFieldValue, ...formik} = useFormik()
useEffect(() => {
      setFieldValue('field', value)
}, [setFieldValue])
layerok commented 4 months ago

I also noticed that calling setValues inside useEffects leads to infinite loop, however if you call setValues with disabled validation, then the there is no loop.

const { data } = useQuery(someQuery);

useEffect(() => {
  if(data) {
    formik.setValues(
      mapDataToFormValues(data),
      false // <-- fixes infinite loop
    );
  }
}, [data, formik]);