Preventing Form Input Lag in Material UI ReactJS
Tags: webdev, react, javascript, frontend
Published: 2023-04-21 01:57:27

Once in my work I stumbled into issue that ruined user experience in my web app. There were severe input lag in my large form that consist of dozen inputs. The input lag came up whenever user typed quickly in text or number input.

That time I was using ReactJS with Material UI as a base design. Material UI is renowned for its beautiful design with easy API. But the cost is that the library itself contribute in making built bundle bloated. Also Material UI is considered heavy when it comes to performance. So optimization become priority when I use Material UI.

Back to the topic, how do I overcome the problem. It turned out I found 2 solutions that could help optimizing the form.

## #1: The Native Way

In order to demonstrate the solution, I already made demo project with the same requirements.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4t528q7ume3kepu1ld6.png)

There are several inputs that represent order data. These inputs are controlled manually by `useState` binding.

```tsx
function OrderForm() {
  const [payloadType, setPayloadType] = React.useState<string>('kg')
  const [payloadWeight, setPayloadWeight] = React.useState<string>('0')
  const [cargo, setCargo] = React.useState<string>('')
  const [originCheckpoint, setOriginCheckpoint] = React.useState<string>('')
  const [destinationCheckpoint, setDestinationCheckpoint] = React.useState<string>('')
  const [driverName, setDriverName] = React.useState<string>('')
  const [truckLicensePlate, setTruckLicensePlate] = React.useState<string>('')
  const adornmentLabel = React.useMemo<string>(
    () => payloadType === 'pound' ? 'lb' : 'kg',
    [payloadType]
  )

  const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
  }

  return (
    <>
      <Typography variant="h2" fontWeight={600} fontSize={24} sx={{ mb: 2 }}>
        Order Form
      </Typography>
      <form onSubmit={submitHandler} style={{ width: '80%', maxWidth: '400px', display: 'block', margin: 'auto' }}>
        <RadioInput
          name='payloadType'
          label='Payload Type'
          onChange={(value) => setPayloadType(value)}
          value={payloadType}
          choices={[
            { label: 'Kg', value: 'kg', },
            { label: 'Pound', value: 'pound', },
          ]}
        />
        <NumberInput
          label='Weight'
          name="weight"
          value={payloadWeight}
          onChange={(value) => setPayloadWeight(value)}
          adornmentLabel={adornmentLabel}
        />
        <TextInput
          label='Cargo'
          name="cargo"
          value={cargo}
          onChange={(value) => setCargo(value)}
        />
        <TextInput
          label='Origin Checkpoint'
          name="originCheckpoint"
          value={originCheckpoint}
          onChange={(value) => setOriginCheckpoint(value)}
        />
        <TextInput
          label='Destination Checkpoint'
          name="destinationCheckpoint"
          value={destinationCheckpoint}
          onChange={(value) => setDestinationCheckpoint(value)}
        />
        <TextInput
          label='Driver Name'
          name="driverName"
          value={driverName}
          onChange={(value) => setDriverName(value)}
        />
        <TextInput
          label='Truck License Plate'
          name="truckLicensePlate"
          value={truckLicensePlate}
          onChange={(value) => setTruckLicensePlate(value)}
        />
        <TextInput label='Other Input 1' name="otherInput1" value={''} onChange={() => { }} />
        <TextInput label='Other Input 2' name="otherInput2" value={''} onChange={() => { }} />
        <TextInput label='Other Input 3' name="otherInput3" value={''} onChange={() => { }} />
        <TextInput label='Other Input 4' name="otherInput4" value={''} onChange={() => { }} />
        <Button type="submit" variant="contained" sx={{ mt: 3 }}>Submit</Button>
      </form>
    </>
  )
}
```

Just ignore the detail implementation of `TextInput`, `NumberInput`, and `RadioInput`. These components are just Material UI `TextField` with some design modification to fit the current design system.

Visually whenever user type quickly in one of the input, there's slight delay before what user typed shows in the screen. But I think it's not wise to judge performance just by checking it visually, we need precise metrics.

I'll use **React DevTools** profiler to measure website performance while typing in one of the inputs. This tool can be installed as a website extension and pretty easy to use. You can read more about this profiler by reading their blog [here](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html).

The performance gonna be measured by defining a simple scenario. I will turn on the profiler and then type a single character in **cargo input** and then turn off the profiler. After that we can judge the performance by looking at the commit histories.

![Before Memo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7u6k6u7h2dl2a73ec0z.gif)

Let's take a look at the result above. I'm using _Ranked_ section since it give clearer result. Then move the commit into second commit. This commit show how many components were re-rendered after I typed one character.

From there we can see that there are many yellow bars. These yellow bars represent components render time. We can see that many yellow bars are shown which conclude that typing one character in **cargo input** results that many components re-render. It costs **37ms** duration in total. So how do we improve it?

As you probably know, there are several techniques in ReactJS to prevent component re-render. In the context of preventing **component** from re-rendering, we can use `memo`. `memo` is a component wrapper that could prevent component re-render by comparing component properties manually. Shortly, if component properties after re-render is not the same as component properties before re-render, then re-render that component. We can implement `memo` in our component input (`TextInput`, `NumberInput`, and `RadioInput`).

```tsx
import { FormControl, FormLabel, TextField } from '@mui/material';
import React from 'react'

interface TextInputProps {
  value: string;
  onChange: (value: string) => void;
  label: string;
  name: string;
}

function TextInput(props: TextInputProps) {
  return (
    <FormControl sx={{ display: 'flex' }}>
      <FormLabel id={props.name} sx={{ textAlign: 'start', mt: 1 }}>
        {props.label}
      </FormLabel>
      <TextField
        aria-labelledby={props.name}
        name={props.name}
        size='small'
        type="text"
        fullWidth
        value={props.value}
        onChange={(e) => props.onChange(e.target.value)}
      />
    </FormControl>
  )
}

// Before memo
// export default TextInput;

// After memo
export default React.memo(TextInput, (prev, next) => {
  return prev.value === next.value
    && prev.name === next.name
    && prev.label === next.label;
})
```

The component above is the implementation of `TextInput`. We can ignore the implementation detail and just focus on component properties and `React.memo`. By default, components are exported by using default export directly. But when using `memo`, component are wrapped first before exported. This wrapper also receive function that receives 2 arguements, previous properties and next properties.

The function acts as a comparator that compare properties before and after re-render. When the function result `true` then the component is not re-rendered, and _vice versa_.

We can see that I didn't compare `onChange` property. FYI, ReactJS treat function and object differently. When ReactJS do comparison for function or object, the result is always different. So we can omit this for now since our `onChange` function will never be changed.

Apply `memo` to all of our input components. Let's see the result.

![After Memo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1is5xw08eisosln29jj.gif)

As you can see, the number of components that are re-rendered are reduced significantly. The duration in total for the second commit are also reduced from **37ms to 17ms**. More than **50%**! Visually, any input delay that happened before is also gone. The form is working smoothly!

## #2: Librarian

If you think it's more convenient to use 3rd party library for doing form. There are several library that help us dealing with form. The most prominent one is **Formik**.

**Formik** helps us deals with form by shortening form and its inputs binding process. But in background, **Formik** implementation is almost the same as native binding. This library can't prevent unnecessary re-render that happen when we type in the form. So additional configuration is needed like I already demonstrated before.

Other library that exists are **React Hook Form**. I personally like this library better. I have used **Formik** and **React Hook Form** in my projects and I think the second win my heart. This library can prevent unnecessary component re-render by using special technique in binding inputs. Not also preventing re-render, this library bundle size is also much smaller than **Formik**. If you are interested more with the comparison, check out [this](https://refine.dev/blog/react-hook-form-vs-formik/) article.

***

## Closing

So that's all from me. What do you think of this article? Let me know your thoughts :D

If you like my article, make sure to click the ❤️ button.