conformの勉強
nextjs14 でconform を動かしてみる。 #Next.js - Qiita
https://qiita.com/masakinihirota/items/2119937371c2db97ed1a
の続き(同じソース)
👆のコードをAIに聞いて記録していく。
※AIも最新の情報だとよくわかってない提案を出してくる。
この記事のリポジトリ
masakinihirota/conform2
conform Next.js
conformの公式サンプル集
conform/examples at main · edmundhung/conform
conformの公式サンプル Next.js
conform/examples/nextjs at main · edmundhung/conform
conformのサンプルをついます。
conformのサンプルには3種類のformがあります。
この3種類のformを使って表示するまでを行います。
3つのサンプル
login
siginup
todo
機能ごとに一つのファイルに纏めてありましたが、
それぞれ個別に独立させて動かします。
インストール
pnpm dlx create-next-app conform2
pnpm install zod
pnpm install @conform-to/react @conform-to/zod --save
基本ファイル 共通 レイアウト メニュー
src\app\layout.tsx
import type { Metadata } from 'next';
import Link from 'next/link';
import '@/styles/globals.css';
export const metadata: Metadata = {
title: 'NextJS - Conform Example'
};
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<main>
<h1>NextJS サンプル</h1>
<br />
<ul>
<li>
<Link href="login">Login 一番シンプルなサンプル</Link>
</li>
</ul>
<hr />
{children}
</main>
</body>
</html>
);
}
src\app\page.tsx
export default function Index({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const value = searchParams['value'];
if (!value) {
return null;
}
return (
<div>
Submitted the following value:
<pre>{JSON.stringify(JSON.parse(value.toString()), null, 2)}</pre>
</div>
);
}
src\styles\globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
フォーム Login 一番シンプルなサンプル
login\actions.ts
'use server';
// このファイルは、ログインフォームの送信アクションを定義します。
// parseWithZod スキーマに基づいてデータを解析し、型安全なオブジェクトに変換します。
import { parseWithZod } from '@conform-to/zod';
import { redirect } from 'next/navigation';
// スキーマ
import { loginSchema } from './schema';
// 非同期
export async function login(prevState: unknown, formData: FormData) {
const submission = parseWithZod(formData, {
// スキーマ
// loginSchemaを使ってバリデーションを行います。
schema: loginSchema
});
// バリデーションが成功しなかった場合
if (submission.status !== 'success') {
// submission.reply() はエラーメッセージを返します。
// reply()メソッドは、バリデーションの結果に基づいてレスポンスを生成します。
return submission.reply();
}
// バリデーションが成功した場合
// リダイレクトします。
// JSON.stringify(submission.value) はフォームの値を文字列に変換します。
redirect(`/?value=${JSON.stringify(submission.value)}`);
}
prevState: 前の状態を表します。
formData: フォームデータを表します。
parseWithZod関数の結果はsubmissionに格納され、そのstatusプロパティがチェックされます。
parseWithZod
Zod でパースを行うための関数です。
login\form.tsx
'use client';
import { useForm, getFormProps, getInputProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState, useFormStatus } from 'react-dom';
import { login } from './actions';
import { loginSchema } from './schema';
function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
// フォームの送信状態を取得します。
// pendingは通信の状態を表す真偽値です。
const { pending } = useFormStatus();
// 送信中か無効化ならボタンを無効化します。
// {...props}は他のプロパティを受け取るためのものです。
return <button {...props} disabled={pending || props.disabled} />;
}
export function LoginForm() {
const [lastResult, action] = useFormState(login, undefined);
const [form, fields] = useForm({
// 最後の送信結果を同期する
lastResult,
// クライアントでのバリデーションロジックを再利用する
onValidate({ formData }) {
return parseWithZod(formData, { schema: loginSchema });
},
// フォームのバリデーションをブラーイベントでトリガーする
// Blur
// フォーカスが外れた時に発生するイベント
shouldValidate: 'onBlur'
});
return (
<form
// Next.js Server Actions
action={action}
// フォーム要素をアクセシブルにするために必要なすべてのプロパティを返す
{...getFormProps(form)}
// 少し見やすくするためのスタイル
className="border-2 border-green-500"
>
<div>
<label>Email</label>
<input
className={!fields.email.valid ? 'error' : ''}
// メールフィールドの入力プロパティを取得する
{...getInputProps(fields.email, { type: 'text' })}
key={fields.email.key}
/>
{/* エラーだと表示される。 */}
<div>{fields.email.errors}</div>
</div>
<div>
<label>Password</label>
<input
className={!fields.password.valid ? 'error' : ''}
{...getInputProps(fields.password, { type: 'password' })}
key={fields.password.key}
/>
{/* エラーだと表示される。 */}
<div>{fields.password.errors}</div>
</div>
<label>
<div>
<span>Remember me</span>
{/* チェックボックス */}
<input {...getInputProps(fields.remember, { type: 'checkbox' })} />
</div>
</label>
<hr />
<Button>Login</Button>
</form>
);
}
- useForm
Conform / useForm
https://ja.conform.guide/api/react/useForm
const [form, fields] = useForm(options);
HTML フォームを強化するためのフォームとフィールドのメタデータを返す React フックです。
このフックは、フォームのID、初期値、バリデーションルール、
バリデーションのタイミング、送信時のコールバック関数など、
さまざまなオプションを提供します。
また、クライアントサイドバリデーションを省略したり、
IDを変更することでフォームをリセットしたりするなどできます。
- getFormProps
const props = getFormProps(form, options);
getFormPropsは、Reactフォーム要素をアクセシビリティ対応にするために必要なすべてのプロパティを返すヘルパー関数です。
使い方
useFormフックを使ってフォームの状態とフィールド情報を持つformとfieldsを取得します。
getFormProps(form)を呼び出し、返されるオブジェクトをform要素のpropsとして渡します。
import { useForm, getFormProps } from '@conform-to/react';
function Example() {
const [form, fields] = useForm();
return <form {...getFormProps(form)} />;
}
オプション
-
ariaAttributes: 結果のプロパティに aria-invalid と aria-describedby を含めるかどうかを決定します。デフォルトは true です。
-
ariaInvalid: aria 属性が meta.errors または meta.allErrors に基づくべきかを決定します。デフォルトは errors です。
-
ariaDescribedBy: 追加の id を aria-describedby 属性に追加します。フィールドのメタデータから meta.descriptionId を渡すことができます。
getFormPropsを使う利点
フォーム要素を手動でアクセシビリティ対応にする手間を省けます。
コードが簡潔になり、読みやすくなります。
一貫したアクセシビリティを保ちやすくなります。
- aria 属性
ARIA 属性は、Web ページを アクセシビリティ 対応にするために使用される属性のセットです。
アクセシビリティ
障害を持つ人々が Web コンテンツを利用できる状態にすることを指します。
Web ページをアクセシビリティ対応にするために使用される ARIA 属性です。
アクセシブル
「接近しやすい」「利用しやすい」「入りやすい」
- role 属性
要素の役割を定義します。例えば、button 要素には role="button" 属性を指定することができます。
- aria-label
要素のラベルテキストを定義します。例えば、画像には aria-label 属性を使用して、画像の内容を説明することができます。
- aria-invalid
要素が 無効な入力 を含んでいることを示します。
スクリーンリーダーなどの支援技術はこの属性を使用して、ユーザーにエラーを伝えることができます。
一般的に、form 要素内の入力フィールドにこの属性が使用されます。
- aria-describedby
要素が 説明 または 追加情報 を提供する別の要素と関連付けられていることを示します。
スクリーンリーダーはこの属性を使用して、ユーザーに関連する情報を提供することができます。
この属性は、エラーメッセージ、ヒント、補足情報など、さまざまな目的に使用できます。
aria-invalid と aria-describedby は、WAI-ARIA というアクセシビリティガイドラインの一部です。
- getInputProps
Conform / getInputProps
https://ja.conform.guide/api/react/getInputProps
入力要素をアクセシブルにするために必要なすべてのプロパティを返すヘルパーです。
const props = getInputProps(meta, options);
login\page.tsx
import { LoginForm } from './form';
export default function Login() {
return <LoginForm />;
}
ルーティングの役割のみ
login\schema.ts
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
remember: z.boolean().optional()
});
loginで必要なスキーマ
.optional()
値が入力されなくてもエラーにならない。
signup サインアップ パスワード判定
signup\actions.ts
'use server';
import { parseWithZod } from '@conform-to/zod';
import { redirect } from 'next/navigation';
import { createSignupSchema } from './schema';
export async function signup(prevState: unknown, formData: FormData) {
// データのバリデーション: ユーザー名が一意であることを確認
const submission = await parseWithZod(formData, {
schema: (control) =>
// コントロールに基づいてzodスキーマを作成
createSignupSchema(
control,
// サーバーでのみ定義されている関数を使用してユーザー名の一意性を確認
{
isUsernameUnique(username) {
// サーバーへのアクセスをシミュレート
return new Promise((resolve) => {
setTimeout(() => {
// ユーザー名がadminである場合はエラーを返す
resolve(username !== 'admin');
}, Math.random() * 300);
});
}
}
),
// 非同期バリデーションを有効にする
async: true
});
// バリデーション結果のチェック: エラーがあればエラーメッセージを返す
if (submission.status !== 'success') {
return submission.reply();
}
// バリデーションが成功した場合、ユーザーをリダイレクトする
redirect(`/?value=${JSON.stringify(submission.value)}`);
}
submission
Zodライブラリを使用してユーザーの登録情報検証結果を表すオブジェクトです。これは、formData(ユーザー入力情報を含む、ユーザー名、パスワードなど)をcreateSignupSchemaで定義された登録スキーマに対して検証した後にparseWithZod関数によって返されるオブジェクトです。
schema: (control) =>
// コントロールに基づいてzodスキーマを作成
createSignupSchema(control, {
isUsernameUnique(username) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(username !== 'admin');
}, Math.random() * 300);
});
}
}),
async: true
ここでは、createSignupSchema関数を使用してZodスキーマを作成しています。
この関数は、ユーザー名が一意であることを確認するための
カスタムバリデーション関数isUsernameUniqueを受け取ります。
この関数は、ユーザー名が'admin'でない場合にtrueを解決するPromiseを返します。
これは、'admin'という管理者名を使わせないようにするためです。
schema: (control) =>:この部分は、schemaという関数を定義しています。
この関数はcontrolという引数を取ります。controlはフォームの各フィールドの値を含むオブジェクトです。
createSignupSchema(control, {...}):この部分でcreateSignupSchema関数を呼び出しています。
この関数はZodスキーマを作成するためのもので、
第一引数にはcontrolオブジェクト、
第二引数にはバリデーションルールを定義したオブジェクトを渡します。
isUsernameUnique(username):この部分は、ユーザー名が一意であることを確認するためのバリデーションルールを定義しています。
この関数はusernameを引数に取り、Promiseを返します。
Promiseは非同期操作の結果を表すオブジェクトで、この場合はユーザー名が一意であるかどうかを確認する非同期操作の結果を表しています。
setTimeout(() => { resolve(username !== 'admin'); }, Math.random() * 300);
この部分で、非同期にユーザー名が一意であるかどうかを確認しています。
setTimeout関数を使って一定時間(0から300ミリ秒のランダムな時間)後にresolve関数を呼び出しています。
これはクライアントからサーバーにアクセスする時間をシミュレートさせています。
resolve関数には、ユーザー名が'admin'でない場合にtrueを、そうでない場合にfalseを渡しています。つまり、ユーザー名が'admin'でなければ一意であるとみなしています。
注意点としては、このコードではユーザー名が一意であるかどうかを確認するための非常に単純なチェックを行っています。
実際のアプリケーションでは、データベースや他の永続的なストレージを確認してユーザー名が一意であるかどうかを確認する必要があります。
signup\form.tsx
'use client';
import { useForm, getFormProps, getInputProps } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState, useFormStatus } from 'react-dom';
import { signup } from './actions';
import { createSignupSchema } from './schema';
function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
const { pending } = useFormStatus();
return <button {...props} disabled={pending || props.disabled} />;
}
export function SignupForm() {
// フォームの状態を管理します。
// signupはサインアップ処理を行う関数で、undefinedは初期状態を示します。
// フォームの最後の結果(lastResult)とアクション(action)を取得します。
const [lastResult, action] = useFormState(signup, undefined);
// フォームの状態とフィールドを管理します。
// フォームの状態(form)とフィールド(fields)を取得します。
const [form, fields] = useForm({
lastResult,
// フォームのバリデーションを行うための関数を定義しています。
// formDataはフォームのデータを含むオブジェクトです。
// parseWithZod関数を使用して、Zodスキーマを使用してフォームデータを解析し、バリデーションを行います。
onValidate({ formData }) {
return parseWithZod(formData, {
// 制約が定義されていないスキーマを作成します
schema: (control) => createSignupSchema(control)
});
},
// バリデーションがいつ行われるべきかを指定します。
// onBlurはフィールドからフォーカスが外れた時
shouldValidate: 'onBlur',
// onInputはフィールドの入力が変更された時
shouldRevalidate: 'onInput'
});
return (
// フォーム要素を作成します。actionはフォームが送信されたときに実行されるアクションを指定します。
// getFormProps(form)はフォームのプロパティを取得します。
<form
action={action}
{...getFormProps(form)}
className="border-2 border-green-500"
>
<label>
<div>Username</div>
{/* 入力フィールドを作成します。 */}
<input
className={!fields.username.valid ? 'error' : ''}
// getInputPropsはフィールドのプロパティを取得します。
{...getInputProps(fields.username, { type: 'text' })}
// keyはReactの要素に一意のキーを提供します。
key={fields.username.key}
/>
{/* エラーの場合表示します。 */}
<div>{fields.username.errors}</div>
</label>
<label>
<div>Password</div>
<input
className={!fields.password.valid ? 'error' : ''}
{...getInputProps(fields.password, { type: 'password' })}
key={fields.password.key}
/>
{/* エラーの場合表示します。 */}
<div>{fields.password.errors}</div>
</label>
<label>
<div>Confirm Password</div>
<input
className={!fields.confirmPassword.valid ? 'error' : ''}
{...getInputProps(fields.confirmPassword, { type: 'password' })}
key={fields.confirmPassword.key}
/>
{/* エラーの場合表示します。 */}
<div>{fields.confirmPassword.errors}</div>
</label>
<hr />
<Button>Signup</Button>
</form>
);
}
signup\schema.ts
import { conformZodMessage } from '@conform-to/zod';
import { z } from 'zod';
import type { Intent } from '@conform-to/react';
// ユーザー登録フォームのバリデーションスキーマを作成するための関数
export function createSignupSchema(
// intentはバリデーションの意図を示します。
intent: Intent | null,
options?: {
// TypeScriptの関数型です。特定の形状の関数を表すための型です。
isUsernameUnique: (username: string) => Promise<boolean>;
}
) {
return z
.object({
// ユーザー名のバリデーション: ユーザー名は文字列である必要があり、英字と数字のみを含むことができます。
// これは、z.string().regex()メソッドを使用して確認されます。
// また、pipeメソッドを使用して、ユーザー名が一意であるかどうかを確認します。
// これは、options.isUsernameUnique関数を使用して行われます。
username: z
.string({ required_error: 'Username is required' })
.regex(
/^[a-zA-Z0-9]+$/,
'Invalid username: only letters or numbers are allowed'
)
// ユーザー名が有効な場合のみ実行されるようにスキーマをパイプします
.pipe(
// superRefineメソッドは、Zodライブラリの一部で、カスタムの検証関数を追加するために使用されます。
// この関数は2つの引数を取ります。最初の引数は検証される値(この場合はusername)で、2つ目の引数は検証のコンテキスト(ctx)です。
z.string().superRefine((username, ctx) => {
// この行は、ユーザー名の検証が必要かどうかを判断します。intentがnullであるか、intent.typeが'validate'であり、かつintent.payload.nameが'username'である場合、ユーザー名の検証が必要と判断されます。
const isValidatingUsername =
intent === null ||
(intent.type === 'validate' &&
intent.payload.name === 'username');
// isValidatingUsername がfalseである場合(つまり、ユーザー名の検証が必要ない場合)、このブロックが実行されます。
// ここでは、ctx.addIssue()メソッドを使用して、検証の問題を追加しています。
// この問題のメッセージは conformZodMessage.VALIDATION_SKIPPEDで、検証がスキップされたことを示します。
if (!isValidatingUsername) {
ctx.addIssue({
code: 'custom',
message: conformZodMessage.VALIDATION_SKIPPED
});
return;
}
// options?.isUsernameUniqueが関数でない場合、このブロックが実行されます。
// これは、isUsernameUnique関数が提供されていない場合にエラーを発生させるためのものです。
// ここでもctx.addIssue()メソッドを使用して、検証の問題を追加しています。
// この問題のメッセージはconformZodMessage.VALIDATION_UNDEFINEDで、検証関数が未定義であることを示します。
// また、fatal: trueというオプションを設定しているため、この問題が発生すると、それ以降の検証は行われません。
if (typeof options?.isUsernameUnique !== 'function') {
ctx.addIssue({
code: 'custom',
message: conformZodMessage.VALIDATION_UNDEFINED,
fatal: true
});
return;
}
// options.isUsernameUnique(username): この関数は、提供されたユーザ名が一意であるかどうかを確認します。
// 一意性は、各ユーザが独自のユーザ名を持つことを保証するために重要です。
// この関数は非同期であり、Promiseを返します。
return (
options
.isUsernameUnique(username)
// .then((isUnique) => {...}): isUsernameUnique関数がPromiseを返すため、.thenメソッドを使用してその結果を処理します。
// isUniqueは、ユーザ名が一意である場合はtrue、そうでない場合はfalseを含むブール値です。
.then((isUnique) => {
if (!isUnique) {
// addIssueメソッドは、問題を報告するために使用されます。
ctx.addIssue({
code: 'custom',
message: 'Username is already used'
});
}
})
);
})
)
})
.and(
z
// パスワードのバリデーション: パスワードと確認用パスワードは文字列である必要があります。
.object({
password: z.string({ required_error: 'Password is required' }),
confirmPassword: z.string({
required_error: 'Confirm password is required'
})
})
// パスワードの一致確認: refineメソッドを使用して、パスワードと確認用パスワードが一致していることを確認します。
// 一致しない場合、エラーメッセージ Password does not match が表示されます。
.refine((data) => data.password === data.confirmPassword, {
message: 'Password does not match',
path: ['confirmPassword']
})
);
}
Todo list 項目の手動での追加や削除 順序の入れ替え
todos\actions.ts
'use server';
import { parseWithZod } from '@conform-to/zod';
import { redirect } from 'next/navigation';
import { todosSchema } from './schema';
export async function createTodos(prevState: unknown, formData: FormData) {
// parseWithZodは、入力データを検証し、その結果を返す関数です。
const submission = parseWithZod(formData, {
schema: todosSchema
});
if (submission.status !== 'success') {
return submission.reply();
}
redirect(`/?value=${JSON.stringify(submission.value)}`);
}
submission: 検証結果を表すオブジェクトです。
reply: エラーメッセージを含むレスポンスを返します。
redirect(/?value=${JSON.stringify(submission.value)});:この行では、検証が成功した場合、ユーザーを新しいURLにリダイレクトします。
新しいURLは、検証されたデータ(submission.value)をクエリパラメータとして含みます。
todos\form.tsx
'use client';
import {
useForm,
getFormProps,
getInputProps,
getFieldsetProps
} from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { useFormState, useFormStatus } from 'react-dom';
import { createTodos } from './actions';
import { todosSchema } from './schema';
function Button(props: React.ButtonHTMLAttributes<HTMLButtonElement>) {
const { pending } = useFormStatus();
return <button {...props} disabled={pending || props.disabled} />;
}
export function TodoForm() {
// createTodosは初期状態を作成するための関数です。
// undefinedは初期状態の値です。
// lastResultは前回のフォーム送信結果を保持しています。
const [lastResult, action] = useFormState(createTodos, undefined);
const [form, fields] = useForm({
lastResult,
// onValidateはフォームデータの検証関数を指定します。
onValidate({ formData }) {
return parseWithZod(formData, { schema: todosSchema });
},
// shouldValidateは検証を行うタイミングを指定します。
shouldValidate: 'onBlur'
});
// タスクのリストを取得します。
const tasks = fields.tasks.getFieldList();
return (
<form
action={action}
{...getFormProps(form)}
className="border-2 border-green-500"
>
<div>
<label>Title</label>
<input
// タイトルフィールドが有効かどうかをチェックします。有効でない場合、エラークラスが適用されます。
className={!fields.title.valid ? 'error' : ''}
// タイトルフィールドのプロパティを取得します。
{...getInputProps(fields.title, { type: 'text' })}
// このコードは、Reactの要素に一意のキーを割り当てています。
// keyはReactが要素を追跡し、再レンダリングが必要な要素を特定するために使用します。
// ここでは、fields.title.keyというオブジェクトのプロパティをキーとして使用しています。
// fieldsはおそらく複数のフィールド情報を持つオブジェクトで、その中のtitleフィールドが選択され、そのkeyプロパティがキーとして使用されています。
// このキーは、Reactが効率的に再レンダリングを行うために重要です。
// 特にリストや配列の要素をレンダリングする際には、各要素に一意のキーを割り当てることが推奨されます。
key={fields.title.key}
/>
<div>{fields.title.errors}</div>
</div>
<hr />
<div className="form-error">{fields.tasks.errors}</div>
{/* タスクのリストをループして、各タスクのフィールドセットを作成します。 */}
{tasks.map((task, index) => {
// タスクのフィールドセットを取得します。
const taskFields = task.getFieldset();
return (
// <fieldset>はHTMLの要素で、タグで定義するフォームの入力項目をグループ化するためのタグです。
// key={task.key}は、Reactが要素を追跡し、再レンダリングが必要な要素を特定するために使用します。ここでは、taskオブジェクトのkeyプロパティをキーとして使用しています。
// {...getFieldsetProps(task)}はJavaScriptのスプレッド構文を使用しています。
// これは、getFieldsetProps(task)関数が返すプロパティを<fieldset>要素に適用します。
// この関数はtaskオブジェクトを引数に取り、おそらくそれに基づいて一連のプロパティを生成します。
// 全体として、このコードは特定のタスクに基づいて<fieldset>要素を作成し、その要素に適切なプロパティを設定します。
<fieldset key={task.key} {...getFieldsetProps(task)}>
<div>
<label>Task #{index + 1}</label>
<input
className={!taskFields.content.valid ? 'error' : ''}
{...getInputProps(taskFields.content, { type: 'text' })}
key={taskFields.content.key}
/>
<div>{taskFields.content.errors}</div>
</div>
<div>
<label>
<span>Completed</span>
<input
className={!taskFields.completed.valid ? 'error' : ''}
{...getInputProps(taskFields.completed, {
type: 'checkbox'
})}
key={taskFields.completed.key}
/>
</label>
</div>
{/* タスクを削除するボタン */}
<Button
{...form.remove.getButtonProps({
name: fields.tasks.name,
index
})}
>
Delete
</Button>
{/* タスクを最上位に移動するボタン */}
<Button
{...form.reorder.getButtonProps({
name: fields.tasks.name,
from: index,
to: 0
})}
>
Move to top
</Button>
{/* タスクの内容をクリアするボタン */}
<Button
{...form.update.getButtonProps({
name: task.name,
value: { content: '' }
})}
>
Clear
</Button>
</fieldset>
);
})}
{/* タスクを追加するボタン */}
<Button {...form.insert.getButtonProps({ name: fields.tasks.name })}>
Add task
</Button>
<hr />
<Button>Save</Button>
</form>
);
}
import {
useForm,
getFormProps,
getInputProps,
getFieldsetProps
} from '@conform-to/react';:ここでは、Conform-toのReactライブラリから関数をインポートしています。
これらの関数はフォームの作成と管理を容易にします。
import { parseWithZod } from '@conform-to/zod';:ここでは、Conform-toのZodライブラリから関数をインポートしています。
Zodはスキーマ検証ライブラリで、フォームの入力値の検証に使用します。
import { useFormState, useFormStatus } from 'react-dom';:ここでは、ReactのDOMライブラリからuseFormStateとuseFormStatusというフックをインポートしています。
これらはフォームの状態とステータスを管理するために使用します。
import { createTodos } from './actions';:ここでは、createTodosというアクションをインポートしています。
これはフォームの送信時に実行されるアクションです。
import { todosSchema } from './schema';:ここでは、todosSchemaというスキーマをインポートしています。
これはZodで作成されたスキーマで、フォームの入力値の検証に使用します。
function Button(props: React.ButtonHTMLAttributes) {...}:ここでは、カスタムボタンコンポーネントを作成しています。
このボタンは、フォームの送信が進行中であるか、またはプロパティで無効化されている場合に無効化されます。
export function TodoForm() {...}:ここでは、Todoフォームコンポーネントを作成しています。
このコンポーネントでは、useFormStateとuseFormフックを使用してフォームの状態とフィールドを管理します。
注意点としては、useFormStateとuseFormフックはコンポーネントのレンダリング中に呼び出す必要があります。また、useFormStatusフックはボタンコンポーネントのレンダリング中に呼び出されます。
これらのフックはReactのルールに従って、コンポーネントのトップレベルで呼び出す必要があります。
useFormState(createTodos, undefined); は、フォームの状態を管理するためのカスタムフックです。
createTodosは初期状態を作成するための関数で、undefinedは初期状態の値です。
useFormは、フォームの状態とフィールドを管理するためのカスタムフックです。
lastResultは前回のフォーム送信結果、onValidateはフォームデータの検証関数、shouldValidateは検証を行うタイミングを指定します。
fields.tasks.getFieldList(); は、タスクのリストを取得します。
は、フォームを作成します。 actionはフォーム送信時のアクション、getFormProps(form)はフォームのプロパティを取得します。fields.title.valid は、タイトルフィールドが有効かどうかをチェックします。有効でない場合、エラークラスが適用されます。
getInputProps(fields.title, { type: 'text' }) は、タイトルフィールドのプロパティを取得します。
fields.title.errors は、タイトルフィールドのエラーを表示します。
tasks.map((task, index) => {...}) は、タスクのリストをループして、各タスクのフィールドセットを作成します。
todos\schema.ts
import { z } from 'zod';
export const taskSchema = z.object({
content: z.string(),
completed: z.boolean().optional()
});
export const todosSchema = z.object({
title: z.string(),
tasks: z.array(taskSchema).nonempty()
});