Open3
Conformをいろいろ触ってみる
はじめに
Remixのフォームライブラリとして最近話題になっているConformがきになるので、色々触ってみる。
Conformの特徴
- プログレッシブ・エンハンスメント・ファーストAPI
- 型安全なフィールド推論
- きめ細かいサブスクリプション
- 組み込みのアクセシビリティ・ヘルパー
- Zodによる自動型強制
Progressive enhancement first APIs
Type-safe field inference
Fine-grained subscription
Built-in accessibility helpers
Automatic type coercion with Zod
チュートリアル
下記サイトを参考にやってみよう
まずは必要なパッケージをインストール
npm install @conform-to/react @conform-to/zod --save
スキーマの定義
Conformのzod統合で空文字が自動的に削除されるから、z.preprocess( (value) => (value === '' ? undefined : value),
みたいな前処理はいらないみたい。
import { z } from 'zod';
const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
message: z
.string({ required_error: 'Message is required' })
.min(10, 'Message is too short')
.max(100, 'Message is too long'),
});
action関数でフォーム解析
- conformが提供する、
parseWithZod
ヘルパーを使えばObject.fromEntories()
の代替になる - フォーム解析の結果は値もエラーも
reply()
でオブジェクトとして返す
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const submission = parseWithZod(formData, { schema });
if (submission.status !== "success") {
return submission.reply();
}
const message = await sendMessage(submission.value);
if (!message.sent) {
return submission.reply({
formErrors: ["Failed to send the message. Please try again later."],
});
}
return redirect("/messages");
}
バリデーションの改善
ユーザーがフォーム入力時に、早期フィードバックをユーザに与えることも可能であり、
shouldValidate
と shouldRevalidate
のオプション設定で実現する。
export default function ContactUs() {
const user = useLoaderData<typeof loader>();
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
// ... previous config
// Validate field once user leaves the field
shouldValidate: 'onBlur',
// Then, revalidate field as user types again
shouldRevalidate: 'onInput',
});
// ...
}
サーバー側で毎回バリデーションはフィードバックに時間がかかるため、クライアントバリデーションの実装でも可能である。onValidate
でサーバー側のロジックをクライアントでも使用しているイメージっぽい。ただし、onSubmit
ハンドラがフォームに必要。
export default function ContactUs() {
const user = useLoaderData<typeof loader>();
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
// ... previous config
// Run the same validation logic on client
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
});
return (
<Form
method="post"
id={form.id}
{/* The `onSubmit` handler is required for client validation */}
onSubmit={form.onSubmit}
aria-invalid={form.errors ? true : undefined}
aria-describedby={form.errors ? form.errorId : undefined}
>
{/* ... */}
</Form>
);
}
完成コード
import {
useForm,
getFormProps,
getInputProps,
getTextareaProps,
} from '@conform-to/react';
import { parseWithZod, getZodConstraint } from '@conform-to/zod';
import { type ActionFunctionArgs } from '@remix-run/node';
import { Form, useActionData } from '@remix-run/react';
import { z } from 'zod';
import { sendMessage } from '~/message';
const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
message: z
.string({ required_error: 'Message is required' })
.min(10, 'Message is too short')
.max(100, 'Message is too long'),
});
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const submission = parseWithZod(formData, { schema });
if (submission.status !== 'success') {
return submission.reply();
}
const message = await sendMessage(submission.value);
if (!message.sent) {
return submission.reply({
formErrors: ['Failed to send the message. Please try again later.'],
});
}
return redirect('/messages');
}
export default function ContactUs() {
const lastResult = useActionData<typeof action>();
const [form, fields] = useForm({
lastResult,
constraint: getZodConstraint(schema),
shouldValidate: 'onBlur',
shouldRevalidate: 'onInput',
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
});
return (
<Form method="post" {...getFormProps(form)}>
<div>
<label htmlFor={fields.email.id}>Email</label>
<input {...getInputProps(fields.email, { type: 'email' })} />
<div id={fields.email.errorId}>{fields.email.errors}</div>
</div>
<div>
<label htmlFor={fields.message.id}>Message</label>
<textarea {...getTextareaProps(fields.message)} />
<div id={fields.message.errorId}>{fields.message.errors}</div>
</div>
<button>Send</button>
</Form>
);
}