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.
The following should update formik.values.value but instead I get an infinite loop.
onChangeValue={value => formik.setFieldValue("value", value)}
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>
);
}
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>
);
}
| Software | Version(s) |
| ---------------- | ---------- |
| Formik | 2.2.1
| React | 16.14.0
| TypeScript | 4.0.3
| Browser | chrome
| npm/Yarn | npm
| Operating System | macOS
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;
| 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
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.