Refactor a React Component from 165 Lines to 30 Lines
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.
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.
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.
<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.
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.
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.
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
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
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.
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.
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
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.
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
Github Repo: https://github.com/Mohammad-Faisal/react-hook-form-material-ui
Have something to say? Get in touch with me via LinkedIn