Рефакторинг компонента React со 165 до 30 строк
- среда, 23 февраля 2022 г. в 00:39:20
React Hook Form — одна из самых популярных библиотек для обработки элементов ввода формы в экосистеме React.
Но добиться ее правильной интеграции может быть непросто, если использовать какую-либо библиотеку компонентов.
Сегодня я покажу вам, как можно интегрировать React Hook Form с различными компонентами Material UI.
Я не буду подробно рассказывать о том, как использовать react-hook-form
. Если же вы еще не знаете, как использовать react-hook-form
, я настоятельно рекомендую вам сначала ознакомиться с этой статьей.
Все, что я могу сказать — вы не пожалеете о том, что изучили эту библиотеку.
Давайте посмотрим на код, с которого мы начнем.
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>
}
FormBadDemo.tsx
Это довольно стандартная форма. Мы использовали несколько наиболее распространенных элементов ввода формы. Но у данного компонента есть некоторые проблемы.
Обработчики onChange
работают однообразно, в повторяющемся режиме. Если бы у нас было несколько текстовых элементов ввода, нам пришлось бы управлять ими по отдельности, а это так утомительно.
Когда нам нужно обрабатывать ошибки, то размеры и сложность таких операций возрастают.
Как вы знаете, react-hook-form
отлично работает со стандартными компонентами ввода HTML. Однако все обстоит иначе, если мы используем различные библиотеки компонентов, такие как Material-UI, Ant design или любые другие.
Для таких случаев react-hook-form экспортирует специальный компонент-обертку под названием Controller
. Если вам известно, как работает этот специальный компонент, то интегрировать его с любой другой библиотекой будет проще простого.
Структура компонента Controller
выглядит следующим образом.
<Controller
name={name}
control={control}
render={({ field: { onChange, value }}) => (
<AnyInputComponent
onChange={onChange}
value={value}
/>
)}
/>
Если вы занимались базовой обработкой форм, то знаете, что для любого компонента ввода важны два поля. Одно из них — value
, а другое — onChange
.
Поэтому наш компонент Controller инжектирует эти два свойства вместе со всем волшебным функционалом react-hook-form в компоненты.
Все остальное работает как по маслу! Давайте посмотрим на это в действии.
Каждому элементу ввода формы нужны два основных свойства — name
и value
. Эти 2 свойства управляют всеми функциональными возможностями.
Итак, добавьте тип для этого. Если вы используете javascript, вам это не понадобится.
export interface FormInputProps {
name: string
label: string
}
FormInputProps.ts
Это самый основной компонент, о котором нужно позаботиться в первую очередь. Ниже представлен изолированный компонент ввода текста, построенный с помощью 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'
/>
)}
/>
)
}
FormInputText.tsx
В этом компоненте мы используем свойство control
для формы react-hook-form
. Оно экспортируется из хука useForm()
библиотеки.
Мы также продемонстрировали, как отображать ошибки. Для остальных компонентов пропустим это для краткости.
Вторым наиболее распространенным компонентом ввода является селектор Radio. Код для интеграции с material-ui выглядит следующим образом.
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>
)
}
Нам нужно иметь массив options
, в который необходимо передать доступные опции для этого компонента.
Если внимательно присмотреться, то будет видно, что эти два компонента в основном схожи по использованию.
Наш следующий компонент - это выпадающий список (Dropdown). Почти любая форма нуждается в каком-либо виде выпадающего списка. Код для компонента Dropdown выглядит следующим образом
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>
)
}
FormInputDropdown.tsx
В этом компоненте мы убрали ошибку с отображением метки. Он будет таким же, как Radio.
Это распространенный, но при этом особенный компонент ввода даты. В Material UI у нас нет ни одного компонента Date
, который работал бы "из коробки". Для этого нам необходимы вспомогательные библиотеки.
Сначала установите эти зависимости
yarn add @date-io/date-fns@1.3.13 @material-ui/pickers@3.3.10 date-fns@2.22.1
Будьте осторожны с версиями. Это может привести к некоторым странностям. Нам также нужно обернуть наш компонент ввода данных специальной оберткой.
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>
)
}
FormInputDate.tsx
Я выбрал date-fns
. Вы можете выбрать другие, например moment
.
Это самый сложный компонент (флажок). Не существует четких примеров использования этого компонента с react-hook-form. Для обработки ввода нам придется немного поработать вручную.
Здесь мы контролируем выбранные состояния, чтобы правильно обрабатывать вводимые данные.
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>
)
}
FormInputCheckbox.tsx
Теперь вы просто даете ему список опций, и все работает как надо!
Наш последний компонент - это компонент Slider (слайдер). Он является достаточно распространенным. Код прост для понимания
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}
/>
)}
/>
</>
)
}
Вы можете настроить функцию handleChange
, чтобы сделать компонент двухсторонним слайдером (полезно для временного диапазона). Просто замените number
на number[]
.
Теперь давайте используем все эти компоненты внутри нашей конечной формы. Это позволит использовать преимущества компонентов для многократного использования, которые мы только что создали.
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>
}
FormDemo.tsx
В итоге наша форма выглядит следующим образом.
React-hooks появились в React с версии 16.8, сегодня они используются уже повсеместно. Всех заинтересованных приглашаем на двухдневный онлайн-интенсив, на котором мы разберемся, как работать с React-hooks, создадим компонент с использованием hooks, а также научимся делать кастомные hooks.Поработаем с react-testing-library и научимся тестировать компоненты и кастомные hooks. Интенсив будет полезен frontend JavaScript разработчикам и начинающим React разработчикам. Регистрация доступна по ссылке.