🐈
react-hook-form×MUIで親ページと子コンポーネントで複雑なバリデーションを行う
使うライブラリなど
・react.js
・next.js
・react-hook-form
・Material Ui
やりたいこと
react-hook-formを使って、親ページでも子コンポーネントでもバリデーションをかけたい。
イメージとしては以下。
子コンポーネントは「氏名」、「年齢」、「職業」を入力するインプットフォームになっており、そのインプットフォーム自体を追加したり削除したりできる。
以下は子コンポーネントのインプットフォームを1つ追加した時の様子です。
ただし、submit時には、インプットフォームが1個以上ないといけないように設定します。
また、インプットフォームに記入がない時は以下のように赤線で教えてくれます。
問題なくsubmitできると以下のようにオブジェクトが作られています。
コード
親ページ
index.tsx
import { Box, Button, Container, TextField } from "@mui/material";
import AddPeople from "../component/people/AddPeople";
import {
FormProvider,
useForm,
Controller,
useFieldArray,
} from "react-hook-form";
export interface PurchaseFormInput {
product_name: string;
price: number;
quantity: number;
receivers: Receiver[];
}
export type Receiver = {
receiver_name: string;
age: number;
occupation: string;
};
const Form = () => {
const methods = useForm<PurchaseFormInput>({
defaultValues: {
receivers: [
{
receiver_name: "",
age: 20,
occupation: "",
},
],
},
});
const { handleSubmit, control } = methods;
const { fields, append, remove } = useFieldArray({
control,
name: "receivers",
});
const removeIndex = (index: number) => {
remove(index);
};
const submit = (data: any) => {
if (data.receivers.length == 0) {
console.log("データがありません");
return;
}
console.log(data); // フォームの内容が入る
};
return (
<Container maxWidth="xs" sx={{ mt: 6 }}>
<FormProvider {...methods}>
<Box component="form" onSubmit={handleSubmit(submit)}>
<Controller
name="product_name"
control={control}
rules={{
required: { value: true, message: "入力が必須の項目です" },
validate: (value) => {
if (value !== null || "") {
return true;
}
return "文字を入力してください";
},
}}
render={({ field, fieldState, formState: { errors } }) => (
<TextField
{...field}
type="product_name"
size="small"
sx={{ width: 500 }}
placeholder="例:えんぴつ"
error={fieldState.invalid}
helperText={
fieldState.invalid ? errors.product_name?.message : ""
}
/>
)}
/>
<Box>
{fields.map((field, index) => {
return (
<AddPeople
key={field.id}
index={index}
removeIndex={removeIndex}
/>
);
})}
</Box>
<button
type="button"
onClick={() => {
append({
receiver_name: "",
age: 20,
occupation: "",
});
}}
>
追加
</button>
<Box textAlign="right">
<Button variant="contained" onClick={handleSubmit(submit)}>
送信
</Button>
</Box>
</Box>
</FormProvider>
</Container>
);
};
export default Form;
子コンポーネント
AddPeople.tsx
import { TextField, Stack } from "@mui/material";
import { Controller, useFormContext } from "react-hook-form";
import { PurchaseFormInput } from "../../pages/index";
interface Receiver {
index: number;
removeIndex: (index: number) => void;
}
const AddPeople = (props: Receiver) => {
const { index, removeIndex } = props;
const { control } = useFormContext<PurchaseFormInput>();
return (
<>
<Controller
name={`receivers.${index}.receiver_name`}
control={control}
rules={{
required: { value: true, message: "必須入力" },
validate: (value) => {
if (value !== null || "") {
return true;
}
return "文字を入力してください";
},
}}
render={({ field, formState: { errors } }) => (
<Stack spacing={2}>
<TextField
{...field}
label="receiver_name"
fullWidth
placeholder="田中太郎"
error={errors.receivers?.[index]?.receiver_name ? true : false}
helperText={errors.receivers?.[index]?.message as string}
/>
</Stack>
)}
/>
<Controller
name={`receivers.${index}.age`}
control={control}
rules={{
required: { value: true, message: "必須入力" },
validate: (value) => {
if (!Number.isNaN(Number(value))) {
return true;
}
return "数字を入力してください(全角不可)";
},
}}
render={({ field, formState: { errors } }) => (
<Stack spacing={2}>
<TextField
{...field}
label="age"
fullWidth
placeholder="20"
error={errors.receivers?.[index]?.receiver_name ? true : false}
helperText={errors.receivers?.[index]?.message as string}
/>
</Stack>
)}
/>
<Controller
name={`receivers.${index}.occupation`}
control={control}
rules={{
required: { value: true, message: "必須入力" },
validate: (value) => {
if (value !== null || "") {
return true;
}
return "職業を入力してください";
},
}}
render={({ field, formState: { errors } }) => (
<Stack spacing={2}>
<TextField
{...field}
label="occupation"
fullWidth
placeholder="学生"
error={errors.receivers?.[index]?.receiver_name ? true : false}
helperText={errors.receivers?.[index]?.message as string}
/>
</Stack>
)}
/>
<button
type={"button"}
onClick={() => removeIndex(index)}
style={{ marginLeft: "16px" }}
>
削除
</button>
</>
);
};
export default AddPeople;
注意ポイント1
react-hook-formでは型があっているか判断するために、useFormにインプットの型があっているか比較するinterfaceやtypeなどを渡してあげないといけません。
今回だとPurchaseFormImputというinterfaceをuseFormに渡しています。
index.tsx
export interface PurchaseFormInput {
product_name: string;
price: number;
quantity: number;
receivers: Receiver[];
}
export type Receiver = {
receiver_name: string;
age: number;
occupation: string;
};
~~~省略~~~
const methods = useForm<PurchaseFormInput>({
defaultValues: {
receivers: [
{
receiver_name: "",
age: 20,
occupation: "",
},
],
},
});
今回は、親ページと子コンポーネントであるAddPeopleの2つでreact-hook-formのバリデーションをかけたいので、PurchaseFormInputの中に子コンポーネントでチェックしてほしいtypeをreceivers: Receiver[];
として渡しています。
注意ポイント2
子コンポーネントのAddpeopleでは、インプットフィールドの箇所のnameプロパティは、以下のようにindexごとの要素を渡します。
AddPeople.tsx
<Controller
name={`receivers.${index}.receiver_name`}
control={control}
コードを記載しているgithub
Discussion