LoginSignup
16
6

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-01

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を使うことで実装が可能です。

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で囲みます。

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

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

参考

16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6