Material UI V5 Landing Page (Taken from https://mui.com) and Formik’s landing page (Taken from https://formik.org)
This will show you an alternative way (Compared to Matt Faircliff and Julian Rachmann) in which you can increase Formik’s performance when you have multiple inputs and typing into an input just becomes unbearable. The cause of this problem is CSS-In-JS and the constant re-rendering of Emotion as Formik re-renders on every keystroke.
This method isn’t necessarily exclusive to Material-UI (Although MUI is being used as an example here, and this works for both: V4.x and V5.x) and can be used with any other UI library, as the concepts are the same. Beware, this is hacky, but works in production.
Here’s the GitHub repo that showcases you how to use it.
The concept is pretty simple: We let the <TextField />
component handle its own internal state, and then we propagate the change to Formik with an onBlur
event. We also add additional safeguards to allow for pre-filled passwords to work. In addition, when you dynamically change an attribute of the TextField
such as name
,we need to have the correct way to propagate it correctly as the onBlur
won’t fire. Or when you change <Formik initialValues>
while having enableReinitialize={true}
.
Wrap your TextField
component with the following code (Uses TypeScript):
import { TextFieldProps, TextField } from "@mui/material";
import { useField } from "formik";
import React, { memo, useEffect, useState } from "react";
import { usePropagateRef } from "./usePropagateRef";
export type PerformantTextFieldProps = Omit<TextFieldProps, "name"> & {
name: string;
/**
* IF true, it will use the traditional method for disabling performance
*/
disablePerformance?: boolean;
loading?: boolean;
min?: number;
max?: number;
};
/**
* This is kind of hacky solution, but it mostly works. Your mileage may vary
*/
export const PerformantTextField: React.FC<PerformantTextFieldProps> = memo(
(props) => {
const [field, meta] = useField(props.name);
const error = !!meta.error && meta.touched;
/**
* For performance reasons (possible due to CSS in JS issues), heavy views
* affect re-renders (Formik changes state in every re-render), bringing keyboard
* input to its knees. To control this, we create a setState that handles the field's inner
* (otherwise you wouldn't be able to type) and then propagate the change to Formik onBlur and
* onFocus.
*/
const [fieldValue, setFieldValue] = useState<string | number>(field.value);
const { disablePerformance, loading, ...otherProps } = props;
usePropagateRef({
setFieldValue,
name: props.name,
value: field.value,
});
/**
* Using this useEffect guarantees us that pre-filled forms
* such as passwords work.
*/
useEffect(() => {
if (meta.touched) {
return;
}
if (field.value !== fieldValue) {
setFieldValue(field.value);
}
// eslint-disable-next-line
}, [field.value]);
const onChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
setFieldValue(evt.target.value);
};
const onBlur = (evt: React.FocusEvent<HTMLInputElement>) => {
const val = evt.target.value || "";
window.setTimeout(() => {
field.onChange({
target: {
name: props.name,
value: props.type === "number" ? parseInt(val, 10) : val,
},
});
}, 0);
};
// Will set depending on the performance props
const performanceProps = disablePerformance
? {
...field,
value: loading ? "Loading..." : fieldValue,
}
: {
...field,
value: loading ? "Loading..." : fieldValue,
onChange,
onBlur,
onFocus: onBlur,
};
return (
<>
<TextField
{...otherProps}
InputProps={{
...((props.type === "number" && {
inputProps: { min: props?.min, max: props?.max },
}) ||
undefined),
}}
error={error}
helperText={meta.touched && meta.error}
{...performanceProps}
/>
</>
);
}
);
We add a helper hook called usePropagateRef to allow us overcome an edge case when the name
attribute of the TextField
changes and data needs to be changed.
import { useEffect, useRef } from 'react';
type UsePropagateRefProps = {
setFieldValue: React.Dispatch<React.SetStateAction<any>>;
name: string;
value: any;
};
export function usePropagateRef(props: UsePropagateRefProps) {
const { name, value, setFieldValue } = props;
/**
* This is a special useRef that is used to propagate Formik's changes
* to the component (the other way around that is done).
*
* This needs to be done whenever the name property changes and the content of the
* component remains the same.
*
* An example is when you have a dynamic view that changes the TextField's name attribute.
* If we don't do this, the useBlur hook will overwrite the value that you left before you
* changed the TextField's value.
*
*/
const flagRef = useRef(true);
useEffect(() => {
if (flagRef.current) {
flagRef.current = false;
return;
}
setFieldValue(value);
// eslint-disable-next-line
}, [name]);
}
This should set you to a much more smooth experience while using the <TextField />
Check it out here: https://github.com/superjose/increase-formik-performance-react
The files to be checked can be found here: src/components/Fields/Form/PerformantTextField/