🌟
useActionStateを使ったフォーム実装
React v19で追加されるuseActionStateについて、MantineUIのuse-form、Conformを使って試す
useActionStateとは
useActionState
はReactDOM.useFormState
を置き換える形で追加されたReactフックです。
詳細については以下PRを参照してください。
このフックはフォームアクションの結果に基づいてstateを更新するためのもので、以下のように書かれます。
const [error, submitAction, isPending] = useActionState(
async (previousState, newName) => {
const error = await updateName(newName);
if (error) {
// You can return any result of the action.
// Here, we return only the error.
return error;
}
// handle success
return null;
},
null,
);
詳細はドキュメントを参照してください。
実装
formAction
action.ts
"use server";
import { parseWithZod } from "@conform-to/zod";
import { redirect } from "next/navigation";
import { formSchema } from "~/app/schema";
export async function formAction(prevState: unknown, formData: FormData) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const submission = parseWithZod(formData, { schema: formSchema });
if (submission.status !== "success") {
return submission.reply();
}
redirect("/success");
}
formSchema
schema.ts
import { z } from "zod";
export const formSchema = z.object({
email: z
.string({ required_error: "メールアドレスは必須です" })
.email("メールアドレスを正しく入力してください"),
message: z
.string({ required_error: "メッセージは必須です" })
.min(4, "メッセージが短すぎます")
.max(20, "メッセージが長すぎます"),
});
Conform
conform-form.tsx
"use client";
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
import { Button, TextInput } from "@mantine/core";
import { useActionState } from "react";
import { formAction } from "~/app/actions";
import { formSchema } from "~/app/schema";
export function ConformForm() {
const [lastResult, action, isPending] = useActionState(formAction, undefined);
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema: formSchema });
},
shouldValidate: "onBlur",
});
return (
<form {...getFormProps(form)} action={action}>
<TextInput
required
withAsterisk
label="Email"
placeholder="your@email.com"
{...getInputProps(fields.email, { type: "email" })}
error={fields.email.errors}
/>
<TextInput
required
withAsterisk
label="Message"
placeholder="message"
{...getInputProps(fields.message, { type: "text" })}
error={fields.message.errors}
/>
<Button loading={isPending} type="submit">
ログイン
</Button>
</form>
);
}
公式ドキュメントやGitHubのリポジトリにサンプルがありますので詳細はこちらを参照してください。
MantineUI
-
form.onSubmit
だとevent?.preventDefault()
によってactionが実行されないため、クライアント側はevent.currentTarget
を使ってバリデーションを行う
-
validateInputOnChange
、validateInputOnBlur
を設定して入力時に検証、form.isValid()
でボタンを非活性にしておくことでonSubmit
の判定が要らなくなる?-
mode: "uncontrolled"
だと動きが安定していなさそうだったためmode: 'controlled'
にした方がよさそう
-
mantine-form.tsx
"use client";
import { Button, Text, TextInput } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form";
import { useActionState } from "react";
import { formAction } from "~/app/actions";
import { formSchema } from "~/app/schema";
export function MantineForm() {
const [state, action, isPending] = useActionState(formAction, undefined);
const form = useForm({
mode: "uncontrolled",
initialValues: {
email: "",
message: "",
},
validate: zodResolver(formSchema),
// validateInputOnChange: true,
validateInputOnBlur: true,
});
return (
<form
action={action}
onSubmit={(e) => {
try {
const data = new FormData(e.currentTarget);
const formData = Object.fromEntries(data);
const validatedField = formSchema.safeParse(formData);
if (!validatedField.success) {
throw new Error("invalidated");
}
} catch (error) {
e.preventDefault();
}
}}
>
<TextInput
required
withAsterisk
label="Email"
placeholder="your@email.com"
name="email"
type="email"
key={form.key("email")}
{...form.getInputProps("email")}
/>
<TextInput
required
withAsterisk
label="Message"
placeholder="message"
name="message"
type="text"
key={form.key("message")}
{...form.getInputProps("message")}
/>
<Button loading={isPending} type="submit">
送信
</Button>
</form>
);
}
まとめ
試してみた感じだとMantineUIのuse-formをそのまま使うのは難しそうで、Conformはそのままでも正しく動いていそう。
今回試したものは以下のGitHubリポジトリで公開しています。
参考
Discussion