MUI v5とReact Hook Form v7でサクッとフォームバリデーションを作る

MUI(Material UI)v5とReact Hook Form v7でサクッとフォームのバリデーションを作る方法をまとめました!
2021.10.23

Material-UI v4とReact Fook Form v6を使い続け、幾多の時が過ぎていた浦島太郎状態の片岡です。

今回は新しいプロジェクトを作る際に、MUI v5(v5でMaterial UIからMUIに名称変更)とReact Hook Form v7を採用した結果今までの苦労はなんだったのか・・・となったのでブログにしました!

準備

MUIとReact Hook Formを使ってフォームを作れるようにしていきます。

Reactプロジェクトを作成

適当にプロジェクトを作ります。

yarn create react-app sample-project --template typescript

ライブラリをインストール

実際に使うライブラリをインストールしていきます。

MUIをインストール

公式に習い下記をインストールします。

yarn add @mui/material @emotion/react @emotion/styled

React Hook Formをインストール

ひとまずReact Hook Formもインストールしておきます。

yarn add react-hook-form

サクッとindex.tsxを直す

CSSをリセットしてくれるCssBaseLineを追加します。

src/index.tsx

import { CssBaseline } from '@mui/material'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  <React.StrictMode>
    <CssBaseline />
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

フォームの作成

MUIを使って適当にフォームを作ります。

src/App.tsx

import { Button, Container, Stack, TextField } from '@mui/material'
import React from 'react'

function App() {
  return (
    <Container maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={3}>
        <TextField required label="メールアドレス" type="email" />
        <TextField required label="お名前" />
        <TextField required label="パスワード" type="password" />
        <Button color="primary" variant="contained" size="large">
          作成
        </Button>
      </Stack>
    </Container>
  )
}

export default App

するとこのようなフォームができました。

React Hook Formを接続する

接続するだけならこれだけでできちゃうんですよね・・・すごいですよね・・・

const { register } = useForm()

return <TextField {...register("name")} />

そして先程作ったフォームにReact Hook Formを接続するとこうなります。

src/App.tsx

import { Button, Container, Stack, TextField } from '@mui/material'
import React from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'

// フォームの型
interface SampleFormInput {
  email: string
  name: string
  password: string
}

function App() {
  const { register, handleSubmit } = useForm<SampleFormInput>()

  // フォーム送信時の処理
  const onSubmit: SubmitHandler<SampleFormInput> = (data) => {
    // バリデーションチェックOK!なときに行う処理を追加
    console.log(data)
  }

  return (
    <Container maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={3}>
        <TextField
          required
          label="メールアドレス"
          type="email"
          {...register('email')}
        />
        <TextField required label="お名前" {...register('name')} />
        <TextField
          required
          label="パスワード"
          type="password"
          {...register('password')}
        />
        <Button
          color="primary"
          variant="contained"
          size="large"
          onClick={handleSubmit(onSubmit)}
        >
          作成
        </Button>
      </Stack>
    </Container>
  )
}

export default App

バリデーションルールを追加する

バリデーションルール作るの面倒なので下記をインストールします。

yupはスキーマベースのバリデーションを行うライブラリで、@hookform/resolversはyupでバリデーションを行うためにインストールします。

yarn add @hookform/resolvers yup

スキーマを作成

このような感じでスキーマを作成しました。

メールアドレス:必須とメールアドレスとして正しいか

お名前:必須

パスワード:必須・最小6文字・英字と数字と記号が最低1文字必要

メソッドチェーンでルールを追加できるのと、メールアドレスのチェックのために正規表現いちいち書かなくていいのが楽で好きです!

import * as yup from 'yup'

// バリデーションルール
const schema = yup.object({
  email: yup
    .string()
    .required('必須だよ')
    .email('正しいメールアドレス入力してね'),
  name: yup.string().required('必須だよ'),
  password: yup
    .string()
    .required('必須だよ')
    .min(6, '少ないよ')
    .matches(
      /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&].*$/,
      'パスワード弱いよ'
    ),
})

React Hook Formに適用

先程作ったschemaの内容をReact Hook Formのバリデーションルールとして使用します。

import { yupResolver } from '@hookform/resolvers/yup'

const { register, handleSubmit } = useForm<SampleFormInput>({
  // 追加
  resolver: yupResolver(schema),
})

全体的にこうなりました。

src/App.tsx

import { yupResolver } from '@hookform/resolvers/yup'
import { Button, Container, Stack, TextField } from '@mui/material'
import React from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import * as yup from 'yup'

// フォームの型
interface SampleFormInput {
  email: string
  name: string
  password: string
}

// バリデーションルール
const schema = yup.object({
  email: yup
    .string()
    .required('必須だよ')
    .email('正しいメールアドレス入力してね'),
  name: yup.string().required('必須だよ'),
  password: yup
    .string()
    .required('必須だよ')
    .min(6, '少ないよ')
    .matches(
      /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&].*$/,
      'パスワード弱いよ'
    ),
})

function App() {
  const { register, handleSubmit } = useForm<SampleFormInput>({
    resolver: yupResolver(schema),
  })

  // フォーム送信時の処理
  const onSubmit: SubmitHandler<SampleFormInput> = (data) => {
    // バリデーションチェックOK!なときに行う処理を追加
    console.log(data)
  }

  return (
    <Container maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={3}>
        <TextField
          required
          label="メールアドレス"
          type="email"
          {...register('email')}
        />
        <TextField required label="お名前" {...register('name')} />
        <TextField
          required
          label="パスワード"
          type="password"
          {...register('password')}
        />
        <Button
          color="primary"
          variant="contained"
          size="large"
          onClick={handleSubmit(onSubmit)}
        >
          作成
        </Button>
      </Stack>
    </Container>
  )
}

export default App

エラー内容を表示させる

先程作ったバリデーションルールでSubmitできないことは確認できます。しかしエラー内容が表示されないので今度はエラー内容を表示していきましょう。

エラー内容を拾ってTextFieldに表示するサンプルです。

const { register, formState: { errors } } = useForm()

return (
  <TextField
    {...register("name")}
    // ↓↓追加↓↓
    error={"name" in errors}
    helperText={errors.name?.message}
  />
)

全体的にこうなりました。

src/App.tsx

import { yupResolver } from '@hookform/resolvers/yup'
import { Button, Container, Stack, TextField } from '@mui/material'
import React from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import * as yup from 'yup'

// フォームの型
interface SampleFormInput {
  email: string
  name: string
  password: string
}

// バリデーションルール
const schema = yup.object({
  email: yup
    .string()
    .required('必須だよ')
    .email('正しいメールアドレス入力してね'),
  name: yup.string().required('必須だよ'),
  password: yup
    .string()
    .required('必須だよ')
    .min(6, '少ないよ')
    .matches(
      /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&].*$/,
      'パスワード弱いよ'
    ),
})

function App() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SampleFormInput>({
    resolver: yupResolver(schema),
  })

  // フォーム送信時の処理
  const onSubmit: SubmitHandler<SampleFormInput> = (data) => {
    // バリデーションチェックOK!なときに行う処理を追加
    console.log(data)
  }

  return (
    <Container maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={3}>
        <TextField
          required
          label="メールアドレス"
          type="email"
          {...register('email')}
          error={'email' in errors}
          helperText={errors.email?.message}
        />
        <TextField
          required
          label="お名前"
          {...register('name')}
          error={'name' in errors}
          helperText={errors.name?.message}
        />
        <TextField
          required
          label="パスワード"
          type="password"
          {...register('password')}
          error={'password' in errors}
          helperText={errors.password?.message}
        />
        <Button
          color="primary"
          variant="contained"
          size="large"
          onClick={handleSubmit(onSubmit)}
        >
          作成
        </Button>
      </Stack>
    </Container>
  )
}

export default App

するとこのようにバリデーションエラーが表示されるようになります。