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

setFieldValue creates infinite loop #2858

Closed
NixBiks opened this issue Nov 4, 2020 · 12 comments
Closed

setFieldValue creates infinite loop #2858

NixBiks opened this issue Nov 4, 2020 · 12 comments

Comments

@NixBiks
Copy link

NixBiks commented Nov 4, 2020

🐛 Bug report

Current Behavior

I have an input component that has some internal state (i.e. the inputs are made on another scale - e.g. values are written in millions instead of units. But the state of interest are always just the units.). This component only takes an initial value and not the current value as props. It also exposes a prop called onChangeValue which is basically a callback with the current value as input.

Expected behavior

The following should update formik.values.value but instead I get an infinite loop.

onChangeValue={value => formik.setFieldValue("value", value)}

Reproducible example

import { useFormik } from "formik";
import * as React from "react";

function CustomInput({ initialValue, scale, onChangeValue, name }) {
  const [value, setValue] = React.useState(initialValue / scale);

  React.useEffect(() => {
    onChangeValue(value * scale);
  }, [value, scale, onChangeValue]);

  return (
    <input value={value} onChange={(event) => setValue(event.target.value)} name={name} />
  );
}

export default function Demo() {
  const initialValue = 100;
  const formik = useFormik({
    initialValues: {
      value: initialValue
    },
    onSubmit: (values) => {
      console.log(JSON.stringify(values, null, 2));
    }
  });

  return (
    <form onSubmit={formik.handleSubmit}>
      <CustomInput
        initialValue={initialValue}
        scale={10}
        name="value"
        onChangeValue={value => formik.setFieldValue("value", value)}
      />
    </form>
  );
}

Solution without formik

The following solution works without using formik

import * as React from "react";

function CustomInput({ initialValue, scale, onChangeValue, name }) {
  const [value, setValue] = React.useState(initialValue / scale);

  React.useEffect(() => {
    onChangeValue(value * scale);
  }, [value, scale, onChangeValue]);

  return (
    <input
      value={value}
      onChange={(event) => setValue(event.target.value)}
      name={name}
    />
  );
}

export default function NoFormikDemo() {
  const initialValue = 100;
  const [value, setValue] = React.useState(initialValue);

  function handleSubmit(event) {
    event.preventDefault();
    console.log(value);
  }

  return (
    <form onSubmit={handleSubmit}>
      <CustomInput
        initialValue={initialValue}
        scale={10}
        onChangeValue={setValue}
      />
    </form>
  );
}

Your environment

Software Version(s)
Formik 2.2.1
React 16.14.0
TypeScript 4.0.3
Browser chrome
npm/Yarn npm
Operating System macOS
@horsemanV
Copy link

I've got something similar with resetForm():

import React from "react";
import { Formik, Form, Field } from "formik";

const Example = ({title, boxes}) => {
    const handleReset = (values, {resetForm, setValues, ...formikBag}) => {
        resetForm();
        //resetForm({}); also causes infinite re-renders
    };

    const renderBoxes = (boxes = []) => boxes.map(({label}, i)  => (<Field key={i} type="checkbox" name={label}/>));

    return (<div style={{width: "100vw", height: "100vh"}}>
        <Formik enableReinitialize={true} initialValues={{title, boxes}} onSubmit={() => console.log("submitted")} onReset={handleReset}>
            {({values}) => (
                <Form>
                    <div>
                        <div>
                            <h1>{title}</h1>
                        </div>
                        <div role="group">
                            {renderBoxes(values.boxes)}
                        </div>
                        <div>
                            <button type="reset">Reset</button>
                        </div>
                    </div>
                </Form>)
            }
        </Formik>
    </div>);
};
export default Example;

Your environment

Software Versions
formik 2.2.1
react 17.0.1
react-dom 17.0.1
react-scripts 4.0.0
npm 6.14.8
node 14.15.0
macOS Catalina 10.15.7
Chrome 86.0.4240.183
Firefox 81.0.2 (64-bit)

@jaredpalmer
Copy link
Owner

When you inline the function prop for onChanheValue you are recreating it on every render, it is then firing an effect when it changes causing the infinite loop. You need to wrap the callback with useCallback before passing it down.

@horsemanV
Copy link

@jaredpalmer, thanks for getting back to us so quickly on this, much appreciated.

In the case of resetForm causing infinite re-renders, wrapping my callback in useCallback doesn't prevent the re-renders, I've posted an updated example below :

import React, {useCallback} from "react";
import { Formik, Form, Field } from "formik";

const Example = ({title, boxes}) => {
    const wrappedHandleReset = useCallback((values, {resetForm, setValues, ...formikBag}) => {
        console.count("resetForm");
        resetForm();
    }, []);

    const renderBoxes = (boxes = []) => boxes.map(({label}, i)  => (<Field key={i} type="checkbox" name={label}/>));
    return (<div style={{width: "100vw", height: "100vh"}}>
        <Formik enableReinitialize={true} initialValues={{title, boxes}} onSubmit={async () => null} onReset={wrappedHandleReset}>
            {({values, handleReset, resetForm}) => {
                return (
                <Form>
                    <div>
                        <div>
                            <h1>{values.title}</h1>
                        </div>
                        <div role="group">
                            {renderBoxes(values.boxes)}
                        </div>
                        <div>
                            {/*All three of these cause the same issue*/}
                            <button type="reset">Reset</button>
                            <button type="button" onClick={handleReset}>Reset (Explicitly Bound to onReset)</button>
                            <button type="button" onClick={(e) => {
                                e.preventDefault();
                                resetForm();
                            } }>Reset (imperative resetForm)</button>
                        </div>
                    </div>
                </Form>)
            }}
        </Formik>
    </div>);
};
export default Example;

I've read the docs, am I missing something here?

@horsemanV
Copy link

@maddhruv, any chance we can reopen this, or reopen the separate issue I had for resetForm?

@NixBiks
Copy link
Author

NixBiks commented Nov 13, 2020

Thanks @jaredpalmer

Just for reference if anyone ends up in this thread. Here is the fix

function Demo() {
  const initialValue = 100;
  const {handleSubmit, setFieldValue} = useFormik({
    initialValues: {
      value: initialValue
    },
    onSubmit: (values) => {
      console.log(JSON.stringify(values, null, 2));
    }
  });

  const handleChangeValue = React.useCallback((value) => {
    setFieldValue("value", value);
  }, [setFieldValue]);

  return (
    <form onSubmit={handleSubmit}>
      <CustomInput
        initialValue={initialValue}
        scale={10}
        onChangeValue={handleChangeValue}
      />
    </form>
  );
}

@maddhruv maddhruv reopened this Nov 13, 2020
@krvajal
Copy link
Contributor

krvajal commented Nov 16, 2020

@horsemanV If I am not mistaken you provided a onReset callback here

<Formik enableReinitialize={true} initialValues={{title, boxes}} onSubmit={async () => null} onReset={wrappedHandleReset}>

which is called every time the form is reseted. In that call callback, you are reseting form again, which is causing the infinite loop

@horsemanV
Copy link

Ah, because resetForm fires an event that then gets caught again by the handler. Great catch, thanks.

@NixBiks
Copy link
Author

NixBiks commented Nov 20, 2020

Btw I actually do have a follow up question on my solution from before. In the solution above it requires "the user" to make sure to wrap the onChangeValue prop as a memoized callback. Instead I'd like to wrap my CustomInput as a formik input like below (except it causes an infinite loop). I'm not sure how to fix that.

function FormikInput(props) {
  const [field, meta, helpers] = useField(props.name);
  //  this causes infinite loop!
  return <CustomInput {...props} onChangeValue={helpers.setValue} />
}

export default function Demo() {
  const initialValue = 100;

  return (
    <Formik
      initialValues={{
        value: initialValue
      }}
      onSubmit={(values) => {
        console.log(JSON.stringify(values, null, 2));
      }}
    >
      <Form>
        <FormikInput
          initialValue={initialValue}
          scale={10}
          name="value"
        />
      </Form>
    </Formik>
  );
}

Another solution could be to fix it directly in CustomInput by removing onValueChange as dependency in the use effect but this is bad practice, right?

  React.useEffect(() => {
    onChangeValue(value * scale);
  }, [value, scale]);  // removed onChangeValue

@johnrom
Copy link
Collaborator

johnrom commented Nov 20, 2020

@mr-bjerre we will eventually figure out how to return stable setters so that this workaround isn't necessary, but for now you can use useEventCallback to create a stable reference of your handleValueChange callback so that it never triggers useEffect.

https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback

Using useEventCallback, you would do:

function FormikInput(props) {
  const [field, meta, { setValue }] = useField(props.name);
  const onChangeValue = useEventCallback(value => setValue(value), [setValue]);

  //  this no longer causes infinite loop
  return <CustomInput {...props} onChangeValue={onChangeValue} />
}

@johnrom
Copy link
Collaborator

johnrom commented Nov 20, 2020

There's a ton of related info here if you're in for a read: #2268

If this answers your question, we can close this as a duplicate of the above issue.

@github-actions
Copy link
Contributor

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 60 days

@devmota
Copy link

devmota commented Nov 21, 2022

I have the same problem using the latest versions of Formik and React, has anyone looked into this issue?

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

No branches or pull requests

7 participants