DEV Community

Cover image for Preventing Form Input Lag in Material UI ReactJS
Mario Prasetya M.
Mario Prasetya M.

Posted on

Preventing Form Input Lag in Material UI ReactJS

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

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

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>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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).

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;
})
Enter fullscreen mode Exit fullscreen mode

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

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

Top comments (0)