Refactor a React Component from 165 Lines to 30 Lines
Tags: react, javascript, aws, programming
Published: 2023-12-13 18:25:33

To read more articles like this, [visit my blog](https://www.mdfaisal.com/blog)


React Hook Form is one of the most popular libraries for handling form inputs in the React ecosystem.

But getting it to integrate properly can be tricky if you use any Component library.

Today I will show you how you can integrate with various components of Material UI with React Hook Form.

Let’s get started.

## Pre Requisite

I will not go into much detail about how to use the `react-hook-form`. If you don’t know how to use `react-hook-form` yet, I strongly suggest you check out this article first.

[**How to Use React Hook Form with TypeScript**
Build Performant and Clean Forms for Your Application javascript.plainenglish.io](https://javascript.plainenglish.io/how-to-use-react-hook-form-with-typescript-2cf597c0c45f)

All I can say is you won’t regret learning this library.

## Starting Code

Let’s see the code that we are going to start with.

```js
import TextField from "@material-ui/core/TextField";
import React, { useState} from "react";
import {
    Button,
    Checkbox,
    FormControlLabel,
    FormLabel,
    MenuItem,
    Radio,
    RadioGroup,
    Select,
    Slider
} from "@material-ui/core";
import {KeyboardDatePicker} from '@material-ui/pickers'

const options = [
    {
        label: 'Dropdown Option 1',
        value:'1'
    },
    {
        label: 'Dropdown Option 2',
        value:'2'
    },
]

const radioOptions = [
    {
        label: 'Radio Option 1',
        value:'1'
    },
    {
        label: 'Radio Option 2',
        value:'2'
    },
]

const checkboxOptions = [
    {
        label: 'Checkbox Option 1',
        value:'1'
    },
    {
        label: 'Checkbox Option 2',
        value:'2'
    },
]

const DATE_FORMAT = 'dd-MMM-yy'

export const FormBadDemo = () => {

    const [textValue , setTextValue] = useState('');
    const [dropdownValue , setDropDownValue] = useState('');
    const [sliderValue , setSliderValue] = useState(0);
    const [dateValue , setDateValue] = useState(new Date());
    const [radioValue , setRadioValue] = useState('');
    const [checkboxValue, setSelectedCheckboxValue] = useState<any>([])

    const onTextChange = (e:any) => setTextValue(e.target.value)
    const onDropdownChange = (e:any) => setDropDownValue(e.target.value)
    const onSliderChange = (e:any) => setSliderValue(e.target.value)
    const onDateChange = (e:any) => setDateValue(e.target.value)
    const onRadioChange = (e:any) => setRadioValue(e.target.value)

    const handleSelect = (value:any) => {
        const isPresent = checkboxValue.indexOf(value)
        if (isPresent !== -1) {
            const remaining = checkboxValue.filter((item:any) => item !== value)
            setSelectedCheckboxValue(remaining)
        } else {
            setSelectedCheckboxValue((prevItems:any) => [...prevItems, value])
        }
    }

    const handleSubmit = () => {
        console.log({
            textValue: textValue,
            dropdownValue: dropdownValue,
            sliderValue: sliderValue,
            dateValue: dateValue,
            radioValue: radioValue,
            checkboxValue: checkboxValue,
        })
    }

    const handleReset = () => {
        setTextValue('')
        setDropDownValue('')
        setSliderValue(0)
        setDateValue(new Date())
        setRadioValue('')
        setSelectedCheckboxValue('')
    }

    return <form>

        <FormLabel component='legend'>Text Input</FormLabel>
        <TextField
            size='small'
            error={false}
            onChange={onTextChange}
            value={textValue}
            fullWidth
            label={'text Value'}
            variant='outlined'
        />

        <FormLabel component='legend'>Dropdown Input</FormLabel>
        <Select id='site-select' inputProps={{ autoFocus: true }} value={dropdownValue} onChange={onDropdownChange} >
            {options.map((option: any) => {
                return (
                    <MenuItem key={option.value} value={option.value}>
                        {option.label}
                    </MenuItem>
                )
            })}
        </Select>

        <FormLabel component='legend'>Slider Input</FormLabel>
        <Slider
            value={sliderValue}
            onChange={onSliderChange}
            valueLabelDisplay='auto'
            min={0}
            max={100}
            step={1}
        />

        <FormLabel component='legend'>Date Input</FormLabel>
        <KeyboardDatePicker
            fullWidth
            variant='inline'
            defaultValue={new Date()}
            id={`date-${Math.random()}`}
            value={dateValue}
            onChange={onDateChange}
            rifmFormatter={(val) => val.replace(/[^[a-zA-Z0-9-]*$]+/gi, '')}
            refuse={/[^[a-zA-Z0-9-]*$]+/gi}
            autoOk
            KeyboardButtonProps={{
                'aria-label': 'change date'
            }}
            format={DATE_FORMAT}
        />

        <FormLabel component='legend'>Radio Input</FormLabel>
        <RadioGroup aria-label='gender' value={radioValue} onChange={onRadioChange}>
            {radioOptions.map((singleItem) => (
                <FormControlLabel value={singleItem.value} control={<Radio />} label={singleItem.label} />
            ))}
        </RadioGroup>

        <FormLabel component='legend'>Checkbox Input</FormLabel>
        <div>
            {checkboxOptions.map(option =>
                <Checkbox checked={checkboxValue.includes(option.value)} onChange={() => handleSelect(option.value)} />
            )}
        </div>

        <Button onClick={handleSubmit} variant={'contained'} > Submit </Button>
        <Button onClick={handleReset} variant={'outlined'}> Reset </Button>

    </form>
}

```

This is a pretty standard form. We have used some most common form inputs. But this component has some problems.

* `onChange` handlers are repetitive. If we had multiple text inputs we needed to manage those individuals which is so frustrating.

* If we want to handle errors then they will explode in size and complexity.

## Main Idea

As you know `react-hook-form` works perfectly with the default input components of HTML. But it’s not the case if we use various component libraries like Material-UI or Ant design or any other for that matter.

To handle those cases what `react-hook-form` does is export a special wrapper component named **Contrtoller**. If you know how this special component works then integrating it with any other library will be a piece of cake.

The skeleton of the Controller component is like the following.

```js
<Controller
    name={name}
    control={control}
    render={({ field: { onChange, value }}) => (
       <AnyInputComponent
          onChange={onChange}
          value={value}
        />
    )}
/>
```

If you have done basic form handling (Which I am sure you have done) then you know that two fields are important for any input component. One is the `value` and another one is the `onChange` .

So our `Controller` component here injects these 2 properties along with all other magic of `react-hook-form` into the components.

Everything else works like a charm! let’s see it in action.

## Form Input Props

Every form input needs two basic properties. they are `name` and `value` . These 2 properties control the functionalities of all the functionalities.

So, add a type for this. If you are using javascript you won’t need this.

```js
export interface FormInputProps {
    name: string
    label: string
}
```

## Text Input

This is the most basic component that we need to take care of first. Following is an isolated Text input component built with material UI.

```js
import React from 'react'
import { Controller, useFormContext } from 'react-hook-form'
import TextField from '@material-ui/core/TextField'
import {FormInputProps} from "./FormInputProps";

export const FormInputText = ({ name, label }: FormInputProps) => {
    const { control } = useFormContext()

    return (
        <Controller
            name={name}
            control={control}
            render={({ field: { onChange, value }, fieldState: { error }, formState }) => (
                <TextField
                    helperText={error ? error.message : null}
                    size='small'
                    error={!!error}
                    onChange={onChange}
                    value={value}
                    fullWidth
                    label={label}
                    variant='outlined'
                />
            )}
        />
    )
}
```

In this component, we are using the `control` property form `react-hook-form` . As we already know this is exported from the `useForm()` hook of the library.

We also showed how to display the errors. For the rest of the components, we will skip this for brevity.

## Radio Input

Our second most common input component is `Radio` . The code for integrating with `material-ui` is like the following.

```js
import React from 'react'
import { FormControl, FormControlLabel, FormHelperText, FormLabel, Radio, RadioGroup } from '@material-ui/core'
import { Controller, useFormContext } from 'react-hook-form'
import {FormInputProps} from "./FormInputProps";

const options = [
    {
        label: 'Radio Option 1',
        value:'1'
    },
    {
        label: 'Radio Option 2',
        value:'2'
    },
]

export const FormInputRadio: React.FC<FormInputProps> = ({ name, label }) => {
    const { control, formState: { errors }} = useFormContext()

    const errorMessage = errors[name] ? errors[name].message : null

    return (
        <FormControl component='fieldset'>
            <FormLabel component='legend'>{label}</FormLabel>
            <Controller
                name={name}
                control={control}
                render={({ field: { onChange, value }, fieldState: { error }, formState }) => (
                    <RadioGroup aria-label='gender' value={value} onChange={onChange}>
                        {options.map((singleItem) => (
                            <FormControlLabel value={singleItem.value} control={<Radio />} label={singleItem.label} />
                        ))}
                    </RadioGroup>
                )}
            />
            <FormHelperText color={'red'}>{errorMessage ? errorMessage : ''}</FormHelperText>
        </FormControl>
    )
}

```

We need to have an `options` array in which we need to pass the available options for that component.

If you look closely you will see that these 2 components are mostly similar in usage.

## Dropdown Input

Our next component is `Dropdown`. Almost any form needs some kind of dropdown. The code for Dropdown the component is like the following

```js
import React from 'react'
import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core'
import { useFormContext, Controller } from 'react-hook-form'
import {FormInputProps} from "./FormInputProps";

const options = [
    {
        label: 'Dropdown Option 1',
        value:'1'
    },
    {
        label: 'Dropdown Option 2',
        value:'2'
    },
]
export const FormInputDropdown: React.FC<FormInputProps> = ({ name, label }) => {
    const { control } = useFormContext()

    const generateSingleOptions = () => {
        return options.map((option: any) => {
            return (
                <MenuItem key={option.value} value={option.value}>
                    {option.label}
                </MenuItem>
            )
        })
    }

    return (
        <FormControl size={'small'}>
            <InputLabel>{label}</InputLabel>
            <Controller
                render={({ field }) => (
                    <Select id='site-select' inputProps={{ autoFocus: true }} {...field}>
                        {generateSingleOptions()}
                    </Select>
                )}
                control={control}
                name={name}
            />
        </FormControl>
    )
}
```

In this component, we have removed the error showing the label. It will be just like the Radio component

## Date Input

This is a common yet special component. In Material UI we don’t have any `Date` component which works out of the box. We need to have some helper libraries.

First, install those dependencies

```js
yarn add @date-io/date-fns@1.3.13 @material-ui/pickers@3.3.10 date-fns@2.22.1
```

Be careful about the versions. Otherwise, it may give some weird issues. We also need to wrap our data input component with a special wrapper.

```js
import React from 'react'
import DateFnsUtils from '@date-io/date-fns'
import {KeyboardDatePicker, MuiPickersUtilsProvider} from '@material-ui/pickers'
import { Controller, useFormContext } from 'react-hook-form'
import {FormInputProps} from "./FormInputProps";
const DATE_FORMAT = 'dd-MMM-yy'

export const FormInputDate = ({ name, label }: FormInputProps) => {
    const { control } = useFormContext()

    return (
        <MuiPickersUtilsProvider utils={DateFnsUtils}>
            <Controller
                name={name}
                control={control}
                render={({ field, fieldState, formState }) => (
                    <KeyboardDatePicker
                        fullWidth
                        variant='inline'
                        defaultValue={new Date()}
                        id={`date-${Math.random()}`}
                        label={label}
                        rifmFormatter={(val) => val.replace(/[^[a-zA-Z0-9-]*$]+/gi, '')}
                        refuse={/[^[a-zA-Z0-9-]*$]+/gi}
                        autoOk
                        KeyboardButtonProps={{
                            'aria-label': 'change date'
                        }}
                        format={DATE_FORMAT}
                        {...field}
                    />
                )}
            />
        </MuiPickersUtilsProvider>
    )
}
```

I have chosen `date-fns`. You can pick others like `moment`.

## Checkbox Input

This is the most tricky component. There are not clear examples of how to use this component with `react-hook-form` . To handle the input we have to do some manual labor.

We are controlling the selected states here to handle the inputs properly.

```js
import React, { useEffect, useState } from 'react'
import { Checkbox, FormControl, FormControlLabel, FormHelperText, FormLabel } from '@material-ui/core'
import { Controller, useFormContext } from 'react-hook-form'
import {FormInputProps} from "./FormInputProps";

const options = [
    {
        label: 'Checkbox Option 1',
        value:'1'
    },
    {
        label: 'Checkbox Option 2',
        value:'2'
    },
]

export const FormInputCheckbox: React.FC<FormInputProps> = ({ name, label }) => {
    const [selectedItems, setSelectedItems] = useState<any>([])
    const { control, setValue, formState: { errors }} = useFormContext()

    const handleSelect = (value:any) => {
        const isPresent = selectedItems.indexOf(value)
        if (isPresent !== -1) {
            const remaining = selectedItems.filter((item:any) => item !== value)
            setSelectedItems(remaining)
        } else {
            setSelectedItems((prevItems:any) => [...prevItems, value])
        }
    }

    useEffect(() => {
        setValue(name, selectedItems)
    }, [selectedItems])

    const errorMessage = errors[name] ? errors[name].message : null

    return (
        <FormControl size={'small'} variant={'outlined'}>
            <FormLabel component='legend'>{label}</FormLabel>

            <div>
                {options.map((option:any) => {
                    return (
                        <FormControlLabel
                            control={
                                <Controller
                                    name={name}
                                    render={({ field: { onChange: onCheckChange } }) => {
                                        return <Checkbox checked={selectedItems.includes(option.value)} onChange={() => handleSelect(option.value)} />
                                    }}
                                    control={control}
                                />
                            }
                            label={option.label}
                            key={option.value}
                        />
                    )
                })}
            </div>

            <FormHelperText>{errorMessage ? errorMessage : ''}</FormHelperText>
        </FormControl>
    )
}
```

Now you just give it a list of options and everything works like a charm!

## Slider Input

Our final component is a `Slider` component. Which is a fairly common component. The code is simple to understand

```js
import React, {ChangeEvent, useEffect} from 'react'
import { FormLabel, Slider} from '@material-ui/core'
import { Controller, useFormContext } from 'react-hook-form'
import {FormInputProps} from "./FormInputProps";

export const FormInputSlider = ({ name, label }: FormInputProps) => {

    const { control , watch} = useFormContext()
    const [value, setValue] = React.useState<number>(30);

    const formValue = watch(name)
    useEffect(() => {
        if (value) setValue(formValue)
    }, [formValue])

    const handleChange = (event: any, newValue: number | number[]) => {
        setValue(newValue as number);
    };

    return (
        <>
            <FormLabel component='legend'>{label}</FormLabel>
            <Controller
                name={name}
                control={control}
                render={({ field, fieldState, formState }) => (
                    <Slider
                        {...field}
                        value={value}
                        onChange={handleChange}
                        valueLabelDisplay='auto'
                        min={0}
                        max={100}
                        step={1}
                    />
                )}
            />
        </>
    )
}
```

You can customize the `handleChange` function to make it a two-end slider component(useful for time-range). Just change the `number` to `number[]`

## Hook Everything Together

Now let’s use all of these components inside our Final Form. Which will take advantage of the reusable components we just made.

```js
import {Button, Paper, Typography} from "@material-ui/core";
import { FormProvider, useForm } from 'react-hook-form'
import {FormInputText} from "./form-components/FormInputText";
import {FormInputCheckbox} from "./form-components/FormInputCheckbox";
import {FormInputDropdown} from "./form-components/FormInputDropdown";
import {FormInputDate} from "./form-components/FormInputDate";
import {FormInputSlider} from "./form-components/FormInputSlider";
import {FormInputRadio} from "./form-components/FormInputRadio";

export const FormDemo = () => {
    const methods = useForm({defaultValues: defaultValues})
    const { handleSubmit, reset } = methods
    const onSubmit = (data) => console.log(data)
    
    return <Paper style={{display:"grid" , gridRowGap:'20px' , padding:"20px"}}>
        <FormProvider {...methods}>
            <FormInputText name='textValue' label='Text Input' />
            <FormInputRadio name={'radioValue'} label={'Radio Input'}/>
            <FormInputDropdown name='dropdownValue' label='Dropdown Input' />
            <FormInputDate name='dateValue' label='Date Input' />
            <FormInputCheckbox name={'checkboxValue'} label={'Checkbox Input'}  />
            <FormInputSlider name={'sliderValue'} label={'Slider Input'}  />
        </FormProvider>
        <Button onClick={handleSubmit(onSubmit)} variant={'contained'} > Submit </Button>
        <Button onClick={() => reset()} variant={'outlined'}> Reset </Button>
    </Paper>
}
```

Finally, Our Form looks like this. Isn’t it great?

I hope you learned something today. Have a great day!



### Resources:

* **Video Format:** [https://www.youtube.com/watch?v=aaj6YCil1p4](https://www.youtube.com/watch?v=aaj6YCil1p4)

* **Github Repo:** [https://github.com/Mohammad-Faisal/react-hook-form-material-ui](https://github.com/Mohammad-Faisal/react-hook-form-material-ui)



**Have something to say? Get in touch with me via [LinkedIn](https://www.linkedin.com/in/56faisal/) or [Personal Website](https://www.mdfaisal.com/)**