MUI(Material UI)とreact-hook-formとの連携パターン【select・checkbox・DatePickerなど】

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 
```

```tsx
import { Controller, useForm } from 'react-hook-form'

const { control, handleSubmit } = useForm({}) // 使用したいメソッド等
```
react-hook-formを使用したいコンポーネントで読み込みます。

## MUIのTextFieldとの連携
MUIのTextFieldとの連携方法は下記の通りです。

```tsx
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
```
https://mui.com/material-ui/react-text-field/

## MUIのCheckBoxとの連携
MUIのCheckBoxとの連携方法は下記の通りです。

```tsx
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
```
https://mui.com/material-ui/react-checkbox/

## MUIのSelectBoxとの連携
MUIのSelectBoxとの連携方法は下記の通りです。

```tsx
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

```

https://mui.com/material-ui/react-select/

## MUIのRadioGroupとの連携
MUIのRadioGroupとの連携方法は下記の通りです。

```tsx
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
```
https://mui.com/material-ui/react-radio-button/

## DatePickerとの連携
MUIのDatePickerとの連携方法は下記の通りです。
日付の日本対応やバリデーションのために`Day.js`を使用しています。

```tsx
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

```
https://mui.com/x/react-date-pickers/getting-started/

https://day.js.org/

今回の例では日付形式のバリデーションも実装しています。

react-hook-formのバリデーションについては、下記の記事でも解説しています。

https://qiita.com/TK-C/items/2a440f38bf15f76254e0

## MUIをラップした独自のコンポーネント
独自のコンポーネントにMUIのパーツを組み込んで実装するケースもあるかと思います。
入力フォーム全体が親コンポーネントで子コンポーネントを使い回すイメージです。
その場合は`useFormContext`を使うことで実装が可能です。

```tsx: Form.tsx
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`で囲みます。

```tsx:OriginalTextField
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のコンポーネントをフォームに盛り込んだコードになります。

```tsx
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

```
動作イメージは下記の通りです。

![mui-reacthookform.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559347/9c6d12e0-d584-fc45-159a-a1117f0106f5.gif)


## 参考

https://react-hook-form.com/

https://mui.com/material-ui/getting-started/overview/