Open3

Conformをいろいろ触ってみる

KeiKei

はじめに

Remixのフォームライブラリとして最近話題になっているConformがきになるので、色々触ってみる。

https://conform.guide/

KeiKei

Conformの特徴

  • プログレッシブ・エンハンスメント・ファーストAPI
  • 型安全なフィールド推論
  • きめ細かいサブスクリプション
  • 組み込みのアクセシビリティ・ヘルパー
  • Zodによる自動型強制

Progressive enhancement first APIs
Type-safe field inference
Fine-grained subscription
Built-in accessibility helpers
Automatic type coercion with Zod

KeiKei

チュートリアル

下記サイトを参考にやってみよう
https://conform.guide/tutorial

まずは必要なパッケージをインストール

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");
}

バリデーションの改善

ユーザーがフォーム入力時に、早期フィードバックをユーザに与えることも可能であり、
shouldValidateshouldRevalidateのオプション設定で実現する。

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>
  );
}