-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Possible perf issues with useField
helpers
#2268
Comments
I noticed this too. Not great. |
Came here with this exact issue - the helper functions change on every render so my otherwise memoised component rerenders because the helper functions are different. Haven't hacked around to check yet, but could the helper functions be memoised in useField?
|
If we change this, users may have depended on the helpers changing instead of depending on values changing. Fixing this would be a breaking change. We should note it somewhere. const formik = useFormikContext();
const [field, meta, { setValue }] = useField('secondaryField');
React.useEffect(() => {
setValue(formik.values.firstField + 1);
}, [setValue]); // whoops, no formik.values It should be an eventCallback though. |
I understand and agree. It would be rare though, since the React.useEffect(() => {
setValue(formik.values.firstField + 1);
}, [setValue]); // whoops, no formik.values instead of this React.useEffect(() => {
setValue(formik.values.firstField + 1);
}, [formik.values.firstField]); since they would be capitalizing on an undocumented side-effect of the way |
Definitely not expected. Just want to make sure it's noted in the release notes on the off-chance someone gets hit by it. There are some moments where I've sadly had to disable |
@rafaelcavalcante , |
Is there a resolution in works for this or we just have to turn off |
@anant-singh I don't think anyone's actively working on this if you'd like to open a PR. |
The same problem with Later edit:
|
Noticing this issue as well, and it's causing several re-renders. Is there an accepted workaround at this point if using |
Stumbled upon this issue and wrote myself a little helper hook to improve performance. Maybe it will help someone: function useFieldFast(props) {
const [field, meta, helpers] = useField(props);
const latestRef = useRef({});
// On every render save newest helpers to latestRef
latestRef.current.setValue = helpers.setValue;
latestRef.current.setTouched = helpers.setTouched;
latestRef.current.setError = helpers.setError;
// On the first render create new function which will never change
// but call newest helper function
if (!latestRef.current.helpers) {
latestRef.current.helpers = {
setValue: (...args) => latestRef.current.setValue(...args),
setTouched: (...args) => latestRef.current.setTouched(...args),
setError: (...args) => latestRef.current.setError(...args)
};
}
return [field, meta, latestRef.current.helpers];
} Still I think it would be beneficial if that was handled by the library. Like when you use |
@mayorandrew do you have a codesanbox or some perf showing your finds? |
@sibelius I do not have a codesandbox with an example. You can just plug my hook and use that in place of useField. I used that in my project (not public) and amount of rerenders declined dramatically. I.e. previously I would have every field re-render on every other field change which cause cascade of expensive input component re-renders like react-select. By making helper callbacks constant, I am allowing React.memo to cut off expensive subtrees. |
so you use your own hook and also React.memo? |
Yes. According to my source code and issue digging, in current formik architecture it is nearly impossible to make useField re-render that component only when the field actualy needs it (e.g. when value or something else related changes). So I am making wrappers for all my fields and rely on React.memo to prevent deeper rerenders, like so:
Edit: fixed typo when passing |
In terms of userland solutions, the above looks good! In terms of solving this in Formik, we'd need to switch to |
can't we use something similar to redux behavior? subscription based approach? |
i try to reperoduce @mayorandrew approach and in the code:
Shouldn't it be And as i couldn't get it working somebody has a small working example of that stuff...? |
@WegDamit you are right, my typo. It should be I have created a demo sandbox here: |
@mayorandrew Your hook was very helpful, thank you! You seriously saved my butt, helping me understand the problem and realize what's possible. In the project I'm on atm I noticed that import { useRef } from 'react'
import { useField } from 'formik'
export default (props) => {
const [field, meta, helpers] = useField(props)
const helperRef = useRef()
helperRef.current = helpers
const helperShim = useRef({
setValue: (args) => helperRef.current.setValue(args),
setTouched: (args) => helperRef.current.setTouched(args),
setError: (args) => helperRef.current.setError(args),
})
const fieldRef = useRef()
fieldRef.current = field
const makeFieldShim = ({ name, value }) => ({
name,
value,
onBlur: (args) => fieldRef.current.onBlur(args),
onChange: (args) => fieldRef.current.onChange(args),
})
const fieldShim = useRef(makeFieldShim(field))
if (
fieldShim.current.value !== field.value ||
fieldShim.current.name !== field.name
) {
fieldShim.current = makeFieldShim(field)
}
return [fieldShim.current, meta, helperShim.current]
} I use this hook in a HOC that mimics Formik's import React, { useRef } from 'react'
import useSmartField from 'form/smart-field-hook'
export default (props) => {
const { name, as, children, ...elementProps } = props
const [field, meta, helpers] = useSmartField(name)
const element = useRef({ cached: as, memoized: React.memo(as) })
return React.createElement(
element.current.memoized,
{ ...elementProps, field, ...meta, ...helpers },
children
)
} So if I have a import React from 'react'
import TextField from '@material-ui/core/TextField'
export default (props) => {
const { label, error, touched, field } = props
return (
<TextField
label={label}
error={touched && error != null}
{...field}
/>
)
} Then I can memoize it and hook it into Formik like: <SmartField as={MyTextField} name="myField" label="My Field" /> I'm currently using this for all my components and it seems to work well, though I'm no React/Formik guru to know it's not falling into some nasty pitfall. Time will tell. I do like that I can use this to wrap all my input components, including more complex ones that I needed the Here's a demo sandbox I forked from @mayorandrew's. |
This issue is much more then just unwanted renders. |
I created a Typescript version of export function useFieldFast<Val>(
propsOrFieldName: string | FieldHookConfig<Val>,
) {
const [field, meta, helpers] = useField(propsOrFieldName);
const actualHelpersRef = useRef<FieldHelperProps<Val>>(helpers);
// On every render save newest helpers to actualHelpersRef
actualHelpersRef.current.setValue = helpers.setValue;
actualHelpersRef.current.setTouched = helpers.setTouched;
actualHelpersRef.current.setError = helpers.setError;
const consistentHelpersRef = useRef<FieldHelperProps<Val>>({
setValue: (...args) => actualHelpersRef.current.setValue(...args),
setTouched: (value: boolean, shouldValidate?: boolean) =>
actualHelpersRef.current.setTouched(value, shouldValidate),
setError: (...args) => actualHelpersRef.current.setError(...args),
});
return [field, meta, consistentHelpersRef.current] as const;
} |
@0xR You have a small typescript issue there with the generics, here's a fix: export function useFieldFast<Val = any>(
propsOrFieldName: string | FieldHookConfig<Val>,
) {
const [field, meta, helpers] = useField<Val>(propsOrFieldName);
const actualHelpersRef = useRef<FieldHelperProps<Val>>(helpers);
// On every render save newest helpers to actualHelpersRef
actualHelpersRef.current.setValue = helpers.setValue;
actualHelpersRef.current.setTouched = helpers.setTouched;
actualHelpersRef.current.setError = helpers.setError;
const consistentHelpersRef = useRef<FieldHelperProps<Val>>({
setValue: (...args) => actualHelpersRef.current.setValue(...args),
setTouched: (value: boolean, shouldValidate?: boolean) =>
actualHelpersRef.current.setTouched(value, shouldValidate),
setError: (...args) => actualHelpersRef.current.setError(...args),
});
return [field, meta, consistentHelpersRef.current] as const;
} |
Coming these implementations above in Typescript could also look like this: /* eslint-disable @typescript-eslint/no-explicit-any */
import { FieldHelperProps, FieldHookConfig, FieldInputProps, useField } from 'formik';
import { useRef } from 'react';
export function useFieldFast<Val = any>(propsOrFieldName: string | FieldHookConfig<Val>) {
const [field, meta, helpers] = useField<Val>(propsOrFieldName);
const fieldRef = useRef<FieldInputProps<Val>>();
fieldRef.current = field;
const makeFieldShim = (field: FieldInputProps<Val>) => ({
...field,
onBlur: (args: any) => fieldRef.current?.onBlur(args),
onChange: (args: any) => fieldRef.current?.onChange(args),
});
const fieldShim = useRef(makeFieldShim(field));
if (fieldShim.current.value !== field.value || fieldShim.current.name !== field.name) {
fieldShim.current = makeFieldShim(field);
}
// On every render save newest helpers to actualHelpersRef
const actualHelpersRef = useRef<FieldHelperProps<Val>>(helpers);
actualHelpersRef.current.setValue = helpers.setValue;
actualHelpersRef.current.setTouched = helpers.setTouched;
actualHelpersRef.current.setError = helpers.setError;
const consistentHelpersRef = useRef<FieldHelperProps<Val>>({
setValue: (value: Val, shouldValidate?: boolean | undefined) =>
actualHelpersRef.current.setValue(value, shouldValidate),
setTouched: (value: boolean, shouldValidate?: boolean) =>
actualHelpersRef.current.setTouched(value, shouldValidate),
setError: (value: Val) => actualHelpersRef.current.setError(value),
});
return [fieldShim.current, meta, consistentHelpersRef.current] as const;
} I am having some luck with this and wrapping the outer components w/ |
I ran into this today, and realized that the functions from So, while this causes the signature of const [field, meta, { setValue, setTouched, setError }] = useField(name);
// callback is re-created every time any value changes in the form
const callback = useCallback(() => {
setValue('x')
setTouched(true)
setError('y')
}, [setValue, setTouched, setError]) This will never re-set the value of const [field, meta] = useField(name);
const { setFieldValue, setFieldTouched, setFieldError } = useFormikContext()
// callback is created only once and never re-creates
const callback = useCallback(() => {
setFieldValue(name, 'x')
setFieldTouched(name, true)
setFieldError(name, 'y')
}, [setFieldValue, setFieldTouched, setFieldError]) You can even wrap that up into a new hook and use as a drop-in replacement: const useFieldNew = (config) => {
const [field, meta] = useField(config);
const { setFieldTouched, setFieldValue, setFieldError } = useFormikContext();
const helpers = useMemo(
() => ({
setValue: (...args) => setFieldValue(field.name, ...args),
setTouched: (...args) => setFieldTouched(field.name, ...args),
setError: (...args) => setFieldError(field.name, ...args)
}),
[setFieldTouched, setFieldValue, setFieldError, field.name]
);
return [field, meta, helpers];
}; Here's a sandbox illustrating: https://codesandbox.io/s/formik-use-global-helpers-hooks-example-2m9cy?file=/src/App.js This may be a way forward for fixing up the original Edited for clarity |
@danieltott, even so the component continues to render even if it is not changed |
@secodigo True, thank you. Edited my original comment to clarify the language. |
This should be if the field definition changes, right, not the field value? A changed value doesn't mean the setter function needs to change (which could in fact invoke necessary useEffect/useCallback client code) |
This has been working for me without any max depth re-render issues (typescript code ahead):
And to use:
|
This should be fixed in #3231 Names have been changed to |
🚀 Feature request
Since
v2.1.0
we can do this:Unfortunately, the
helpers
object (and thehelpers.setValue
) gets a different reference on every form update, regardless of whether the field has been updated or not. This means that if a single field got updated, then every other field that utilizeshelpers.setValue
as a prop value will have to re-render as well, regardless of whether it implementsReact.memo
or not. For example the following component will always re-render on every form update:This issue is similar to 1804
Current Behavior
The
helpers
object and specifically thehelpers.setValue
gets a different reference on every form updateDesired Behavior
The
helpers.setValue
should only update when the value of the related form field got updatedSuggested Solution
There needs to be some sort of memoization based on the field's value, unrelated to the rest of the form. I haven't gotten into the v2 release, so I can't suggest something.
Who does this impact? Who is this for?
People that utilize
setValue
in component props and people that use heavy/big forms with multiple fieldsThe text was updated successfully, but these errors were encountered: