react-hook-formのuseFieldArray
を使ってフォーム内の要素が動的に変化するフォームを作成しようと思います。
またフォームの初期値にはapiから取得したレスポンスを設定します。
環境
@mui/material 5.15.13
next 14.1.3
react 18
swr 2.2.5
zod 3.23.6
フォームの定義
作成するフォームでは名前、メールアドレス、電話番号を入力するのでzod
を使って下記のように定義します。
const formValueSchema = z.object({
users: z.array(
z.object({
name: z.string().min(1, { message: "名前を入力してください" }),
email: z.string().min(1, { message: "メールアドレスを入力してください" }),
phone: z.string().min(1, { message: "電話番号を入力してください" }),
})
),
});
type FormValue = z.infer<typeof formValueSchema>;
ユーザー取得
次にjsonplaceholder
からユーザー情報を取得します。
const Page: NextPageWithLayout = () => {
const { data, error, isLoading } = useSWR<User[]>(
"https://jsonplaceholder.typicode.com/users",
fetcher
);
return <></>;
};
Page.getLayout = (page: ReactElement) => {
return <AuthLayout>{page}</AuthLayout>;
};
export default Page;
フォーム制御
フォームの状態管理のために必要な定数をuseForm
, useFieldarray
から取得します。
またuseEffect
を使ってフォームの初期値にAPIからのレスポンスを設定しています。
const formValueSchema = z.object({
users: z.array(
z.object({
name: z.string().min(1, { message: "名前を入力してください" }),
email: z.string().min(1, { message: "メールアドレスを入力してください" }),
phone: z.string().min(1, { message: "電話番号を入力してください" }),
})
),
});
type FormValue = z.infer<typeof formValueSchema>;
const Page: NextPageWithLayout = () => {
const { data, error, isLoading } = useSWR<User[]>(
"https://jsonplaceholder.typicode.com/users",
fetcher
);
const defaultValue = {
name: "",
email: "",
phone: "",
};
const {
control,
handleSubmit,
reset,
formState: { isValid },
} = useForm<FormValue>({
resolver: zodResolver(formValueSchema),
defaultValues: {
users: [defaultValue],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "users",
});
useEffect(() => {
if (data) {
reset({
users: data.map((user) => ({
name: user.name,
email: user.email,
phone: user.phone,
})),
});
}
}, [reset, data]);
const onSubmit = (formValue: FormValue) => {
console.log(formValue);
};
return <></>;
};
Page.getLayout = (page: ReactElement) => {
return <AuthLayout>{page}</AuthLayout>;
};
export default Page;
フォーム表示
初期化したデータを元にフォームを構築します。
const formValueSchema = z.object({
users: z.array(
z.object({
name: z.string().min(1, { message: "名前を入力してください" }),
email: z.string().min(1, { message: "メールアドレスを入力してください" }),
phone: z.string().min(1, { message: "電話番号を入力してください" }),
})
),
});
type FormValue = z.infer<typeof formValueSchema>;
const Page: NextPageWithLayout = () => {
const { data, error, isLoading } = useSWR<User[]>(
"https://jsonplaceholder.typicode.com/users",
fetcher
);
const defaultValue = {
name: "",
email: "",
phone: "",
};
const {
control,
handleSubmit,
reset,
formState: { isValid },
} = useForm<FormValue>({
resolver: zodResolver(formValueSchema),
defaultValues: {
users: [defaultValue],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "users",
});
useEffect(() => {
if (data) {
reset({
users: data.map((user) => ({
name: user.name,
email: user.email,
phone: user.phone,
})),
});
}
}, [reset, data]);
const onSubmit = (formValue: FormValue) => {
console.log(formValue);
};
if (error) {
return <div>failed to load</div>;
}
if (isLoading || !data) {
return <div>loading...</div>;
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>
<Typography>Name</Typography>
</TableCell>
<TableCell>
<Typography>Email</Typography>
</TableCell>
<TableCell>
<Typography>Phone</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<Controller
name={`users.${index}.name`}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
error={fieldState.invalid}
helperText={fieldState.error?.message}
fullWidth
/>
)}
/>
</TableCell>
<TableCell>
<Controller
name={`users.${index}.email`}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
error={fieldState.invalid}
helperText={fieldState.error?.message}
fullWidth
/>
)}
/>
</TableCell>
<TableCell>
<Controller
name={`users.${index}.phone`}
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
error={fieldState.invalid}
helperText={fieldState.error?.message}
fullWidth
/>
)}
/>
</TableCell>
<TableCell>
<Button variant="outlined" onClick={() => remove(index)}>
delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Button variant="contained" onClick={() => append(defaultValue)}>
add
</Button>
<Button variant="contained" type="submit">
save
</Button>
</form>
);
};
Page.getLayout = (page: ReactElement) => {
return <AuthLayout>{page}</AuthLayout>;
};
export default Page;
参考