ReactのUIフレームワークであるMUI(Material UI)とフォーム系のライブラリのreact-hook-formを連携してフォームを作成する方法になります。
- TextField
- CheckBox
- SelectBox
- RadioGroup
- DatePicker
- MUIをラップした独自のコンポーネント
上記のMUIのコンポーネントごとに解説していきます。
react-hook-formのインストールなど
インストールのコマンドは下記の通りです。
npm install react-hook-form
#OR
yarn add react-hook-form
import { Controller, useForm } from 'react-hook-form'
const { control, handleSubmit } = useForm({}) // 使用したいメソッド等
react-hook-formを使用したいコンポーネントで読み込みます。
MUIのTextFieldとの連携
MUIのTextFieldとの連携方法は下記の通りです。
const Form = () => {
return (
<Controller
name="text"
control={control}
defaultValue=""
rules={{
required: { value: true, message: '必須入力' }
}}
render={({ field, formState: { errors } }) => (
<TextField
{...field}
label="text"
fullWidth
placeholder="012345678"
error={errors.text ? true : false}
helperText={errors.text?.message as string}
/>
)}
/>
)
}
export default Form
MUIのCheckBoxとの連携
MUIのCheckBoxとの連携方法は下記の通りです。
const Form = () => {
return (
<Box>
<Controller
name="check"
control={control}
defaultValue={true}
render={({ field, formState: { errors } }) => (
<FormGroup {...field}>
<FormLabel component="legend">チェックボックス</FormLabel>
<FormControlLabel
control={<Checkbox name="check" />}
label="チェックボックス"
value={field.value}
/>
<FormHelperText>{errors.check?.message || ''}</FormHelperText>
</FormGroup>
)}
/>
</Box>
)
}
export default Form
MUIのSelectBoxとの連携
MUIのSelectBoxとの連携方法は下記の通りです。
const Form = () => {
return (
<Box>
<Controller
name="select"
control={control}
defaultValue={0}
render={({ field, formState: { errors } }) => (
<FormControl fullWidth error={errors.select ? true : false}>
<InputLabel id="select-label">セレクトボックス</InputLabel>
<Select
labelId="select-label"
id="select"
label="Select"
{...field}
>
<MenuItem value={0}>未選択</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>{errors.select?.message || ''}</FormHelperText>
</FormControl>
)}
/>
</Box>
)
}
export default Form
MUIのRadioGroupとの連携
MUIのRadioGroupとの連携方法は下記の通りです。
const Form = () => {
return (
<Box>
<Controller
name="radio"
control={control}
render={({ field, formState: { errors } }) => (
<FormControl {...field} error={errors.radio ? true : false}>
<FormLabel id="radio-group-label">ラジオボタン</FormLabel>
<RadioGroup row aria-labelledby="radio-group-label" name="radio">
<FormControlLabel value="1" control={<Radio />} label="radio1" />
<FormControlLabel value="2" control={<Radio />} label="radio2" />
<FormControlLabel value="3" control={<Radio />} label="radio3" />
</RadioGroup>
<FormHelperText>{errors.radio?.message || ''}</FormHelperText>
</FormControl>
)}
/>
</Box>
)
}
export default Form
DatePickerとの連携
MUIのDatePickerとの連携方法は下記の通りです。
日付の日本対応やバリデーションのためにDay.js
を使用しています。
import { DesktopDatePicker, LocalizationProvider } from '@mui/x-date-pickers'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import ja from 'dayjs/locale/ja'
import dayjs from 'dayjs'
const Form = () => {
return (
<Box>
<Controller
name="date"
control={control}
rules={{
validate: (value) => {
const formatDate = dayjs(value).format('YYYY/MM/DD')
if (!dayjs(formatDate).isValid()) {
return '日付形式が間違っています'
}
}
}}
render={({ field, formState: { errors } }) => (
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={ja}>
<DesktopDatePicker
{...field}
disableMaskedInput
inputFormat={'YYYY/MM/DD'}
value={field.value || ''}
renderInput={(
params: JSX.IntrinsicAttributes & TextFieldProps
) => (
<TextField
{...params}
inputProps={{
...params.inputProps,
placeholder: dayjs().format('YYYY/M/DD')
}}
label="Date"
error={errors.date ? true : false}
helperText={errors.date?.message as string}
/>
)}
/>
</LocalizationProvider>
)}
/>
</Box>
)
}
export default Form
今回の例では日付形式のバリデーションも実装しています。
react-hook-formのバリデーションについては、下記の記事でも解説しています。
MUIをラップした独自のコンポーネント
独自のコンポーネントにMUIのパーツを組み込んで実装するケースもあるかと思います。
入力フォーム全体が親コンポーネントで子コンポーネントを使い回すイメージです。
その場合はuseFormContext
を使うことで実装が可能です。
import { Box, Button, Container } from '@mui/material'
import OriginalTextField from 'components/OriginalTextField'
import { FormProvider, useForm } from 'react-hook-form'
const Form = () => {
const methods = useForm({})
const { handleSubmit } = methods
const submit = (data) => {
console.log(data) // フォームの内容が入る
}
return (
<Container maxWidth="xs" sx={{ mt: 6 }}>
{/* { FormProviderでフォームの全体を囲む } */}
<FormProvider {...methods}>
<Box component="form" onSubmit={handleSubmit(submit)}>
<Box>
{/* { 独自コンポーネント } */}
<OriginalTextField />
</Box>
<Box textAlign="right">
<Button variant="contained" onClick={handleSubmit(submit)}>
送信
</Button>
</Box>
</Box>
</FormProvider>
</Container>
)
}
export default Form
まずはフォーム全体をFormProvider
で囲みます。
import { TextField } from '@mui/material'
import { Controller, useFormContext } from 'react-hook-form'
const OriginalTextField = () => {
const { control } = useFormContext()
return (
<Controller
name="text"
control={control}
defaultValue=""
rules={{
required: { value: true, message: '必須入力' }
}}
render={({ field, formState: { errors } }) => (
<TextField
{...field}
label="text"
fullWidth
placeholder="012345678"
error={errors.text ? true : false}
helperText={errors.text?.message as string}
/>
)}
/>
)
}
export default OriginalTextField
フォームの子コンポーネントからはuseFormContext
を使うことで親のフォームにアクセスすることができます。
react-hook-formを使うことで、コンポーネント間でのpropsのバケツリレーをせずに済みます。
コード全体
本記事でご紹介したMUIのコンポーネントをフォームに盛り込んだコードになります。
import {
Box,
Button,
Checkbox,
Container,
FormControl,
FormControlLabel,
FormGroup,
FormHelperText,
InputLabel,
MenuItem,
Radio,
RadioGroup,
Select,
Stack,
TextField,
TextFieldProps
} from '@mui/material'
import { DesktopDatePicker, LocalizationProvider } from '@mui/x-date-pickers'
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import dayjs from 'dayjs'
import ja from 'dayjs/locale/ja'
import { Controller, useForm } from 'react-hook-form'
type Form = {
text: string
check: boolean
select: number
radio: number
date: Date
}
const Form = () => {
const { control, handleSubmit } = useForm<Form>({})
const submit = (data) => {
console.log(data) // フォームの内容が入る
}
return (
<Container maxWidth="xs" sx={{ mt: 6 }}>
<Box component="form" onSubmit={handleSubmit(submit)}>
<Stack spacing={4} border={1} borderRadius={1} borderColor="#333" p={3}>
{/* { TextField } */}
<Box>
<Controller
name="text"
control={control}
defaultValue=""
rules={{
required: { value: true, message: '必須入力' }
}}
render={({ field, formState: { errors } }) => (
<TextField
{...field}
label="text"
fullWidth
placeholder="012345678"
error={errors.text ? true : false}
helperText={errors.text?.message as string}
/>
)}
/>
</Box>
{/* { CheckBox } */}
<Box>
<Controller
name="check"
control={control}
defaultValue={true}
render={({ field, formState: { errors } }) => (
<FormGroup {...field}>
<FormControlLabel
control={<Checkbox name="check" />}
label="チェックボックス"
value={field.value}
/>
<FormHelperText>{errors.check?.message || ''}</FormHelperText>
</FormGroup>
)}
/>
</Box>
{/* { SelectBox } */}
<Box>
<Controller
name="select"
control={control}
rules={{
validate: (form) => {
if (!form) {
return '選択をしてください'
}
}
}}
defaultValue={0}
render={({ field, formState: { errors } }) => (
<FormControl fullWidth error={errors.select ? true : false}>
<InputLabel id="select-label">select</InputLabel>
<Select
labelId="select-label"
id="select"
label="Select"
{...field}
>
<MenuItem value={0}>未選択</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>
{errors.select?.message || ''}
</FormHelperText>
</FormControl>
)}
/>
</Box>
<Box>
<Controller
name="radio"
control={control}
rules={{
required: { value: true, message: '必須入力' }
}}
render={({ field, formState: { errors } }) => (
<FormControl {...field} error={errors.radio ? true : false}>
<RadioGroup
row
aria-labelledby="radio-group-label"
name="radio"
>
<FormControlLabel
value="1"
control={<Radio />}
label="radio1"
/>
<FormControlLabel
value="2"
control={<Radio />}
label="radio2"
/>
<FormControlLabel
value="3"
control={<Radio />}
label="radio3"
/>
</RadioGroup>
<FormHelperText>{errors.radio?.message || ''}</FormHelperText>
</FormControl>
)}
/>
</Box>
{/* { DatePicker } */}
<Box>
<Controller
name="date"
control={control}
rules={{
validate: (value) => {
const formatDate = dayjs(value).format('YYYY/M/DD')
if (!dayjs(formatDate).isValid()) {
return '日付形式が間違っています'
}
}
}}
render={({ field, formState: { errors } }) => (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={ja}
>
<DesktopDatePicker
{...field}
disableMaskedInput
inputFormat={'YYYY/M/DD'}
value={field.value || ''}
renderInput={(
params: JSX.IntrinsicAttributes & TextFieldProps
) => (
<TextField
{...params}
inputProps={{
...params.inputProps,
placeholder: dayjs().format('YYYY/M/DD')
}}
label="Date"
fullWidth
error={errors.date ? true : false}
helperText={errors.date?.message as string}
/>
)}
/>
</LocalizationProvider>
)}
/>
</Box>
<Box textAlign="right">
<Button variant="contained" onClick={handleSubmit(submit)}>
送信
</Button>
</Box>
</Stack>
</Box>
</Container>
)
}
export default Form
動作イメージは下記の通りです。
参考