バリデーション
Conform は異なるバリデーションモードをサポートしています。このセクションでは、異なる要件に基づいてフォームをバリデーションする方法を説明します。
#サーバーバリデーション
フォームを完全にサーバーサイドでバリデーションすることができます。これはフォームの送信に限らず、ユーザーがタイピングしているときやフィールドを離れるときにも機能します。これにより、バリデーションロジックをクライアントバンドルから除外することができます。しかし、ユーザーがタイピングしている間にバリデーションを行いたい場合、ネットワークの遅延が懸念されるかもしれません。
1import { useForm } from '@conform-to/react';
2import { parseWithZod } from '@conform-to/zod';
3import { z } from 'zod';
4
5export async function action({ request }: ActionArgs) {
6 const formData = await request.formData();
7 const submission = parseWithZod(formData, {
8 schema: z.object({
9 email: z.string().email(),
10 message: z.string().max(100),
11 }),
12 });
13
14 if (submission.status !== 'success') {
15 return submission.reply();
16 }
17
18 return await signup(data);
19}
20
21export default function Signup() {
22 // サーバーによって返された最後の結果
23 const lastResult = useActionData<typeof action>();
24 const [form] = useForm({
25 // 最後の送信の結果を同期する
26 lastResult,
27
28 // 各フィールドをいつ検証するかを設定する
29 shouldValidate: 'onBlur',
30 shouldRevalidate: 'onInput',
31 });
32
33 // ...
34}
#クライアントバリデーション
クライアントサイドでバリデーションロジックを再利用し、即時のフィードバックを提供することができます。
1import { useForm } from '@conform-to/react';
2import { parseWithZod } from '@conform-to/zod';
3
4// スキーマ定義をアクションの外に移動する
5const schema = z.object({
6 email: z.string().email(),
7 message: z.string().max(100),
8});
9
10export async function action({ request }: ActionArgs) {
11 const formData = await request.formData();
12 const submission = parseWithZod(formData, { schema });
13
14 // ...
15}
16
17export default function Signup() {
18 const lastResult = useActionData<typeof action>();
19 const [form] = useForm({
20 lastResult,
21 shouldValidate: 'onBlur',
22 shouldRevalidate: 'onInput',
23
24 // クライアント バリデーションの設定
25 onValidate({ formData }) {
26 return parseWithZod(formData, { schema });
27 },
28 });
29
30 // ...
31}
#非同期バリデーション
Conform は、少し異なる方法で非同期バリデーションをサポートしています。別のエンドポイントにリクエストを送る代わりに、必要に応じてサーバーバリデーションにフォールバックします。
以下は、メールアドレスがユニークであるかを検証する例です。
1import { refine } from '@conform-to/zod';
2
3// スキーマを共有する代わりに、スキーマクリエーターを準備します。
4function createSchema(
5 options?: {
6 isEmailUnique: (email: string) => Promise<boolean>;
7 },
8) {
9 return z
10 .object({
11 email: z
12 .string()
13 .email()
14 // メールアドレスが有効な場合にのみ実行されるようにスキーマをパイプします。
15 .pipe(
16 // 注意:ここでのコールバックは非同期にはできません。
17 // クライアント上でzodのバリデーションを同期的に実行するためです。
18 z.string().superRefine((email, ctx) => {
19 // これにより、バリデーションが定義されていないことを示すことで、
20 // Conformはサーバーバリデーションにフォールバックします。
21 if (typeof options?.isEmailUnique !== 'function') {
22 ctx.addIssue({
23 code: 'custom',
24 message: conformZodMessage.VALIDATION_UNDEFINED,
25 fatal: true,
26 });
27 return;
28 }
29
30 // ここに到達した場合、サーバー上でバリデーションが行われているはずです。
31 // 結果をプロミスとして返すことで、Zodに非同期であることを知らせます。
32 return options.isEmailUnique(email).then((isUnique) => {
33 if (!isUnique) {
34 ctx.addIssue({
35 code: 'custom',
36 message: 'Email is already used',
37 });
38 }
39 });
40 }),
41 ),
42 }),
43 // ...
44}
45
46export function action() {
47 const formData = await request.formData();
48 const submission = await parseWithZod(formData, {
49 // `isEmailUnique()` が実装された zod スキーマを作成します。
50 schema: createSchema({
51 async isEmailUnique(email) {
52 // ...
53 },
54 }),
55
56 // サーバー上で非同期バリデーションを有効にします。
57 // クライアントバリデーションは同期的でなければならないため、
58 // クライアントでは `async: true` を設定しません。
59 });
60
61 // ...
62}
63
64export default function Signup() {
65 const lastResult = useActionData();
66 const [form] = useForm({
67 lastResult,
68 onValidate({ formData }) {
69 return parseWithZod(formData, {
70 // isEmailUnique()を実装せずにスキーマを作成します。
71 schema: createSchema(),
72 });
73 },
74 });
75
76 // ...
77}
#バリデーションのスキップ
スキーマはすべてのフィールドを一緒に検証します。これは、特に非同期バリデーションの場合、実行コストがかかることがあります。一つの解決策は、送信の意図をチェックすることにより、バリデーションを最小限に抑えることです。
1import { parseWithZod, conformZodMessage } from '@conform-to/zod';
2
3function createSchema(
4 // `intent` は `parseWithZod` ヘルパーによって提供されます。
5 intent: Intent | null,
6 options?: {
7 isEmailUnique: (email: string) => Promise<boolean>;
8 },
9) {
10 return z
11 .object({
12 email: z
13 .string()
14 .email()
15 .pipe(
16 z.string().superRefine((email, ctx) => {
17 const isValidatingEmail =
18 intent === null ||
19 (intent.type === 'validate' && intent.payload.name === 'email');
20
21 // これによってバリデーションがスキップされたことを示すことで、
22 // Conformは前の結果を使用するようになります。
23 if (!isValidatingEmail) {
24 ctx.addIssue({
25 code: 'custom',
26 message: conformZodMessage.VALIDATION_SKIPPED,
27 });
28 return;
29 }
30
31 if (typeof options?.isEmailUnique !== 'function') {
32 ctx.addIssue({
33 code: 'custom',
34 message: conformZodMessage.VALIDATION_UNDEFINED,
35 fatal: true,
36 });
37 return;
38 }
39
40 return options.isEmailUnique(email).then((isUnique) => {
41 if (!isUnique) {
42 ctx.addIssue({
43 code: 'custom',
44 message: 'Email is already used',
45 });
46 }
47 });
48 }),
49 ),
50 }),
51 // ...
52}
53
54export async function action({ request }: ActionArgs) {
55 const formData = await request.formData();
56 const submission = await parseWithZod(formData, {
57 // Retrieve the intent by providing a function instead
58 schema: (intent) =>
59 createSchema(intent, {
60 async isEmailUnique(email) {
61 // ...
62 },
63 }),
64
65 async: true,
66 });
67
68 // ...
69}
70
71export default function Signup() {
72 const lastResult = useActionData();
73 const [form] = useForm({
74 lastResult,
75 onValidate({ formData }) {
76 return parseWithZod(formData, {
77 // 上記のアクションと同様です。
78 schema: (intent) => createSchema(intent),
79 });
80 },
81 });
82
83 // ...
84}