Очистка React компонентов с помощью React Hook Form и Material UI
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
Ввод Text
Это самый основной компонент, о котором нужно позаботиться в первую очередь. Ниже представлен изолированный компонент ввода текста, построенный с помощью 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
Вторым наиболее распространенным компонентом ввода является селектор 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). Почти любая форма нуждается в каком-либо виде выпадающего списка. Код для компонента 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.
Ввод Date
Это распространенный, но при этом особенный компонент ввода даты. В 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
.
Ввод Checkbox
Это самый сложный компонент (флажок). Не существует четких примеров использования этого компонента с 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
Наш последний компонент - это компонент 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 разработчикам. Регистрация доступна по ссылке.
Комментарии (8)
nickD
22.02.2022 20:20-1Просветите плиз, какие преимущества React Hook Form, перед antd form?
DanUnited
24.02.2022 11:14Принципы работы схожи, но react hook form умеет работать с обычными даже html полями, в antd нужны обертки и обязательное наличие свойств value, onChange компонентов. Более гибкая работа с ошибками к примеру, их можно вывести куда угодно без проблем, в antd попробуйте такое же реализовать с пару строк. В третьих это очень маленькая и быстрая библиотека а не гигант.
valera545
24.02.2022 11:12Извините, но заявленный в заголовке результат не достигнут. 30 строк — это только сборка, а декомпозированные инпуты в общей сложности ещё и на большее потянут, чем прежние 165. Не собираюсь спорить с тем, что декомпозиция — хороший тон, что декомпозированный код лучше читается и вообще гибче и удобнее, чем лапша, но это всё не значит, что мы уменьшили объём кода в 5-6 раз. Статья хорошая, но заголовок кликбейтный, а это — зло.
nin-jin
А написали бы сразу на $mol у вас бы получилось ещё меньше кода:
И не пришлось бы рефакторить.
RealPeha
Абракадабра какая-то