Formik: setFieldValue creates infinite loop

Created on 4 Nov 2020  路  11Comments  路  Source: formium/formik

馃悰 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

User Land stale

Most helpful comment

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.

All 11 comments

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) |

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.

@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?

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

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>
  );
}

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

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

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

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

@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} />
}

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.

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

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Jucesr picture Jucesr  路  3Comments

najisawas picture najisawas  路  3Comments

sibelius picture sibelius  路  3Comments

dearcodes picture dearcodes  路  3Comments

jaredpalmer picture jaredpalmer  路  3Comments