Next.js + ReactHookForm (RHF) + Zod + MUI でバリデーションの効く UI を作る
フレームワークおまかせバリバリのバリデーションが効く UI を作ります! (ただし、テキスト入力だけ…その他は気が向いたら…)
きれいな設計としては以下参考文献がおすすめ。この文書を作る際にかなり参照させていただきました!良テキスト非常に感謝です。
ラクス斉藤様著:React Hook Form v7 + MUI v5 + zod v3を使ったコンポーネント実装例
概要
- RHF と MUI、Zod の組み合わせでの UI 作成を Next.js で実施しようとした際のポイントをまとめた
- とりあえず手っ取り早く動かしたい際に、最低限理解しとくポイントも記載
利用 OSS のバージョン
- Next.js : 13.4.12
- React : 18.2.0
- React Hook Form : 7.45.3
- MUI : 5.14.3
- Zod : 3.21.4
インストール
ひとまず Next.js を利用するのでインストールします。プロジェクト名や設定はお好みで!
$ npx create-next-app@latest
MUI をインストール
$ npm i @mui/material @emotion/react @emotion/styled
RHF をインストール
$ npm i react-hook-form
Zod、RHF のための Resolvers をインストール
$ npm i zod @hookform/resolvers
CustomTemplate を利用しないならこれだけで Next.js 上で MUI を利用することができます。*1
バリデーション実現!
MUI の TextField 2つに RHF + Zod でバリデーションを効かせてみます。
以下、Next.js で利用する際のポイント。
- MUI を使うページでは先頭に
”use client”;
をつける。MUI は現状 Client Component でしか動作しないので必須。*2 - TextField に必ず
id
つける。styled の問題らしいが深追いしてない。。。
また、MUI + RHF + Zod の連携について、少し理解すべきことが多かったので以下にポイントをまとめます。
zod.infer
で型作ってuseForm
にわたすと、defaultValues
などで型推論効いて便利defaultValues
で各値を漏れなく初期化するの超重要。 詳細はJunsei Nagao様著:React Hook Form と Zod を使うときの注意点- Controlled なコンポーネントは RHF の Controller でラップする必要がある。TextField は Controlled、UnControlled どちらでも行けるようだが*3、RHF と組にするとなぜか Controlled 扱いでないと動かなかったため、ラップが必要 *4
- Controller と TextField の紐づけのため
inputRef
にfield.ref
を渡す - Controller の
render
メソッドで渡されるfieldState
にバリデーション結果が入ってくる。それを MUI のhelperText
とerror
に渡せば良しなにしてくれる
"use client"; import { TextField } from "@mui/material"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; const schema = z.object({ min3: z.string().min(3, "3文字以上で入力してください!"), num: z.string().regex(/^[0-9]+$/, "数字以外が入力されています!"), }); type FormType = z.infer<typeof schema>; export default function Home() { const { control } = useForm<FormType>({ mode: "onChange", resolver: zodResolver(schema), defaultValues: { min3: "abc", num: "" }, }); return ( <> <main> <Controller name={"min3"} control={control} render={({ field, fieldState }) => ( <TextField inputRef={field.ref} id="alpha" size="small" variant="outlined" {...field} helperText={fieldState.error?.message ?? ""} error={!!fieldState.error} ></TextField> )} ></Controller> <Controller name={"num"} control={control} render={({ field, fieldState }) => ( <TextField inputRef={field.ref} id="text2" size="small" variant="outlined" {...field} helperText={fieldState.error?.message ?? ""} error={!!fieldState.error} ></TextField> )} ></Controller> </main> </> ); }
実行結果:
コンポーネント化
このままでは格好悪いので( MUI のコンポーネント使うごとにラップは面倒くさい)、最低限まとめます。ページトップに挙げた参考文献のように分けるときれいなのですが、とりあえず MUI の素のコンポーネントっぽく使えることを最低限として、簡素でシンプルにしてみました。
import TextField, { TextFieldProps } from "@mui/material/TextField"; import React from "react"; import { Control, Controller, FieldValue, FieldValues } from "react-hook-form"; type TextFieldElementProps<T extends FieldValues> = TextFieldProps & { name: FieldValue<T>; control: Control<T>; }; const TextFieldElement = <T extends FieldValues>({ name, control, ...textFieldProps }: TextFieldElementProps<T>) => { return ( <Controller name={name} control={control} render={({ field, fieldState }) => ( <TextField inputRef={field.ref} {...field} {...textFieldProps} error={!!fieldState.error?.message} helperText={fieldState.error?.message ?? ""} ></TextField> )} ></Controller> ); }; export default TextFieldElement;
使う側:
"use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import TextFieldElement from "@/elements/TextFieldElement"; const schema = z.object({ min3: z.string().min(3, "3文字以上で入力してください!"), num: z.string().regex(/^[0-9]+$/, "数字以外が入力されています!"), }); type FormType = z.infer<typeof schema>; export default function Home() { const { control } = useForm<FormType>({ mode: "onChange", resolver: zodResolver(schema), defaultValues: { min3: "abc", num: "" }, }); return ( <> <main> <TextFieldElement name={"min3"} control={control} id="min3-text" size="small" variant="outlined" ></TextFieldElement> <TextFieldElement name={"num"} control={control} id="num-text" size="small" variant="outlined" ></TextFieldElement> </main> </> ); }
割りと素の MUI コンポーネントみたいに使えるようになったのではないでしょうか!
ただ、Controller や TextField などで利用されるプロパティを使う側から渡してしまうと( onChange
とか )、意図したようには動作しないので注意が必要です。この辺は RHF を普通に使う場合も同じかもしれませんが。
ではでは。