paint-brush
Improving Formik Performance when it's Slow (Material UI)by@josejaviasilis
8,652 reads
8,652 reads

Improving Formik Performance when it's Slow (Material UI)

by Jose Javi AsilisJanuary 11th, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

An alternative way to increase Formik’s performance is shown. The problem is the constant re-rendering of Emotion. 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. 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. This is kind of hacky, but it mostly works.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Improving Formik Performance when it's Slow (Material UI)
Jose Javi Asilis HackerNoon profile picture

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 - TL;DR

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 TextFieldsuch 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}.

The solution

Wrap your TextField component with the following code (Uses TypeScript):


PerformantTextField.tsx

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.


usePropagateRef.ts


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


GitHub Repo

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/