formisch について
formischは、複数のフレームワークに対応するヘッドレスなスキーマベースのフォームライブラリFormischを、AIエージェントが扱うためのスキルです。Valibotによる検証、フォーム状態、型安全なフィールド操作を解説します。
主な機能
- 01Valibotスキーマによる検証
- 02フォームとフィールドの状態管理
- 03変更状態とエラーのヘルパー
- 04動的なフィールド配列
- 05TypeScriptの型推論
- 068つのフレームワークのガイド
活用シーン
- 01各種フレームワークでの検証付きフォーム実装
- 02編集状態と初期値の追跡
- 03動的リストを含むフォームの構築
- 04React NativeでのFormisch利用
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ open-circle/ agent-skills --skill formischインストール後、Claude Code で/formischを入力して読み込まれたか確認します。
同梱ファイル.claude-plugin/plugin.json
詳細
Formisch
このスキルは、AI エージェントが最新のフレームワーク向けスキーマベースのヘッドレスフォームライブラリである Formisch を効果的に使用できるよう支援します。
このスキルを使用するタイミング
- ユーザーが Formisch を使ったフォーム処理について尋ねたとき
- フォームの状態やバリデーションを管理するとき
- Angular、Preact、Qwik、React、React Native、Solid、Svelte、Vue のフォームを扱うとき
- Valibot のスキーマをフォームに統合するとき
はじめに
Formisch は、複数のフレームワークで動作するスキーマベースのヘッドレスフォームライブラリです。主な特長は次のとおりです。
- 小さなバンドルサイズ — 約 2.5 kB から
- スキーマベースのバリデーション — 型安全なバリデーションに Valibot を使用
- ヘッドレス設計 — UI を完全に制御可能
- 型安全性 — 自動補完を含む完全な TypeScript サポート
- フレームワークネイティブ — サポートされる各フレームワークでネイティブなパフォーマンスを実現
サポートされるフレームワーク
| フレームワーク | パッケージ | フック/プリミティブ |
|---|---|---|
| Angular | @formisch/angular | injectForm |
| Preact | @formisch/preact | useForm |
| Qwik | @formisch/qwik | useForm$ |
| React | @formisch/react | useForm |
| React Native | @formisch/react-native | useForm |
| SolidJS | @formisch/solid | createForm |
| Svelte | @formisch/svelte | createForm |
| Vue | @formisch/vue | useForm |
インストール
1. Valibot をインストールする(ピア依存関係)
npm install valibot
2. 使用するフレームワーク用の Formisch をインストールする
npm install @formisch/react # React
npm install @formisch/angular # Angular
npm install @formisch/vue # Vue
npm install @formisch/solid # SolidJS
npm install @formisch/preact # Preact
npm install @formisch/svelte # Svelte
npm install @formisch/qwik # Qwik
npm install @formisch/react-native # React Native
基本概念
スキーマファースト設計
すべてのフォームは Valibot のスキーマから始まります。型はスキーマから自動的に推論されます。
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(
v.string("Please enter your email."),
v.nonEmpty("Please enter your email."),
v.email("The email address is badly formatted."),
),
password: v.pipe(
v.string("Please enter your password."),
v.nonEmpty("Please enter your password."),
v.minLength(8, "Your password must have 8 characters or more."),
),
});
フォームストア
フォームストアは、フォームのすべての状態を管理します。フレームワーク固有のフック/プリミティブを使ってアクセスします。
フォームストアのプロパティ:
isSubmitting— フォームを現在送信中isSubmitted— フォームの送信が試みられたisValidating— バリデーションを実行中isTouched— 1 つ以上のフィールドがフォーカスされたisEdited— 1 つ以上のフィールドが編集されたisDirty— 1 つ以上のフィールドが初期値と異なるisValid— すべてのフィールドがバリデーションに合格しているerrors— ルートレベルのバリデーションエラー
フィールドストア
各フィールドには、次の項目を含む独自のリアクティブストアがあります。
path— フィールドへのパス配列input— 現在のフィールド値errors— フィールド固有のエラーisTouched— フィールドがフォーカスされたisEdited— フィールド値が編集されたisDirty— フィールド値が初期値と異なるisValid— フィールドがバリデーションに合格しているprops— ネイティブ要素に展開するプロパティ(Angular では代わりに[formischControl]でコントロールを接続します)onChange(React と React Native)/onInput(Solid、Svelte、Preact、Qwik)/setInput(Angular) — フィールドの入力値をプログラムで設定します。フィールドをネイティブ要素に接続できない場合に使用します。Vue ではfield.inputを直接設定します(たとえばv-modelを使用します)。
ストアのリアクティビティはフレームワークによって異なります。React、React Native、Solid、Svelte、Vue では、通常のリアクティブプロパティとして公開されます。Angular のプロパティはシグナルであり、通常の値である path を除き、field.errors() のように呼び出します。Preact と Qwik のプロパティはシグナルです。条件式や通常の TypeScript ロジックでは .value を使用してください。あるフレームワークのアクセス構文を別のフレームワークにコピーしないでください。
変更追跡
Formisch は各フィールドについて、次の 2 つの入力を追跡します。
- 初期入力 — 変更追跡の基準(サーバーの状態)
- 現在の入力 — ユーザーが編集中の値(クライアントの状態)
現在の入力が初期入力と異なると、isDirty は true になります。
フレームワークの例
Angular の例
Angular では、JSX コンポーネント API の代わりにシグナル、依存性注入、ディレクティブを使用します。
import { Component } from "@angular/core";
import {
FormischControl,
FormischField,
FormischForm,
injectForm,
type SubmitHandler,
} from "@formisch/angular";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
@Component({
selector: "app-login",
imports: [FormischForm, FormischField, FormischControl],
template: `
<form [formischForm]="loginForm" [formischSubmit]="handleSubmit">
<ng-container *formischField="['email'] of loginForm; let field">
<input [formischControl]="field" type="email" />
@if (field.errors(); as errors) {
<div>{{ errors[0] }}</div>
}
</ng-container>
<button type="submit" [disabled]="loginForm.isSubmitting()">Login</button>
</form>
`,
})
export class LoginComponent {
readonly loginForm = injectForm({ schema: LoginSchema });
readonly handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
}
ネイティブコントロールの同期は [formischControl] に任せてください。ラジオボタンまたはチェックボックスのグループで value が選択肢を識別する場合を除き、競合する [value] または [checked] のバインディングを追加しないでください。
React Native の例
React Native には DOM の <form> 要素も Formisch の Form コンポーネントもありません。handleSubmit を使用し、field.props を TextInput にバインドします。
import { Field, handleSubmit, useForm } from "@formisch/react-native";
import { Button, TextInput, View } from "react-native";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginScreen() {
const loginForm = useForm({ schema: LoginSchema });
const submitForm = handleSubmit(loginForm, (output) => console.log(output));
return (
<View>
<Field of={loginForm} path={["email"]}>
{(field) => (
<TextInput
{...field.props}
value={field.input}
autoCapitalize="none"
keyboardType="email-address"
/>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<TextInput {...field.props} value={field.input} secureTextEntry />
)}
</Field>
<Button title="Login" onPress={submitForm} />
</View>
);
}
React Native のテキスト入力は制御されるため、必ず value={field.input} を渡してください。スイッチ、スライダー、ピッカー、および文字列以外の値には field.onChange(value) を使用します。
React の例
import { Field, Form, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output); // { email: string, password: string }
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit" disabled={loginForm.isSubmitting}>
{loginForm.isSubmitting ? "Submitting..." : "Login"}
</button>
</Form>
);
}
Vue の例
<script setup lang="ts">
import { Field, Form, useForm } from "@formisch/vue";
import type { SubmitHandler } from "@formisch/vue";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = useForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<template>
<Form :of="loginForm" @submit="handleSubmit">
<Field :of="loginForm" :path="['email']" v-slot="field">
<div>
<input v-model="field.input" v-bind="field.props" type="email" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<Field :of="loginForm" :path="['password']" v-slot="field">
<div>
<input v-model="field.input" v-bind="field.props" type="password" />
<div v-if="field.errors">{{ field.errors[0] }}</div>
</div>
</Field>
<button type="submit">Login</button>
</Form>
</template>
SolidJS の例
import { Field, Form, createForm } from "@formisch/solid";
import type { SubmitHandler } from "@formisch/solid";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default function LoginPage() {
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
return (
<Form of={loginForm} onSubmit={handleSubmit}>
<Field of={loginForm} path={["email"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<Field of={loginForm} path={["password"]}>
{(field) => (
<div>
<input {...field.props} value={field.input} type="password" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
)}
</Field>
<button type="submit">Login</button>
</Form>
);
}
Svelte の例
<script lang="ts">
import { createForm, Field, Form } from '@formisch/svelte';
import type { SubmitHandler } from '@formisch/svelte';
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const loginForm = createForm({
schema: LoginSchema,
});
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
console.log(output);
};
</script>
<Form of={loginForm} onsubmit={handleSubmit}>
<Field of={loginForm} path={['email']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="email" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<Field of={loginForm} path={['password']}>
{#snippet children(field)}
<div>
<input {...field.props} value={field.input} type="password" />
{#if field.errors}
<div>{field.errors[0]}</div>
{/if}
</div>
{/snippet}
</Field>
<button type="submit">Login</button>
</Form>
Qwik の例
import { Field, Form, useForm$ } from "@formisch/qwik";
import { component$ } from "@qwik.dev/core";
import * as v from "valibot";
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
export default component$(() => {
const loginForm = useForm$(() => ({
schema: LoginSchema,
}));
return (
<Form of={loginForm} onSubmit$={(output) => console.log(output)}>
<Field
of={loginForm}
path={["email"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="email" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<Field
of={loginForm}
path={["password"]}
render$={(field) => (
<div>
<input {...field.props} value={field.input.value} type="password" />
{field.errors.value && <div>{field.errors.value[0]}</div>}
</div>
)}
/>
<button type="submit">Login</button>
</Form>
);
});
フォームの設定
const form = useForm({
// Required: Valibot schema
schema: MySchema,
// Optional: Initial values (partial allowed)
initialInput: {
email: "[email protected]",
},
// Optional: Empty values for required fields without initial input
// Required strings default to ''; number, boolean, and date to undefined
emptyInput: {
number: 0,
},
// Optional: When first validation occurs
// Options: 'initial' | 'touch' | 'input' | 'change' | 'blur' | 'submit' (default)
validate: "submit",
// Optional: When a field is validated again once it already has an
// error or the form has been submitted
// Options: 'touch' | 'input' (default) | 'change' | 'blur' | 'submit'
revalidate: "input",
});
Qwik では、useForm$ に設定を返す関数を渡す必要があります。例:useForm$(() => ({ schema: MySchema }))。これにより、Qwik が設定を QRL に変換できるようになります。
省略可能および nullable なフィールドは undefined のままです。emptyInput は、入力が undefined である必須フィールドにのみフォールバック値を設定します。
フィールドパス
パスは、スキーマ内のフィールドを参照する型安全な配列です。
// Top-level field
<Field of={form} path={['email']} />
// Nested field (schema: { user: { email: string } })
<Field of={form} path={['user', 'email']} />
// Array item field (schema: { todos: [{ label: string }] })
<Field of={form} path={['todos', 0, 'label']} />
// Dynamic array index
{items.map((item, index) => (
<Field of={form} path={['todos', index, 'label']} key={item} />
))}
フォームのメソッド
すべてのメソッドで、一貫した API パターンに従います。
- 第 1 引数:フォームストア
- 第 2 引数:設定オブジェクト
値の読み取り
import {
getDeepError,
getDeepErrorEntries,
getDeepErrorEntry,
getDeepErrors,
getErrors,
getInput,
} from "@formisch/react";
// Get field value
const email = getInput(form, { path: ["email"] });
// Get entire form input
const allInputs = getInput(form);
// Get field errors
const emailErrors = getErrors(form, { path: ["email"] });
// Get all errors across all fields (including form-level errors)
const allErrors = getDeepErrors(form);
// Get all errors of a field and its descendants
const todoErrors = getDeepErrors(form, { path: ["todos"] });
// Get every error together with its field path
const errorEntries = getDeepErrorEntries(form);
// Get only the first error of a field and its descendants
const firstTodoError = getDeepError(form, { path: ["todos"] });
// Get only the first error together with its field path
const firstErrorEntry = getDeepErrorEntry(form);
フォームレベルの form.errors と getErrors(form) には、ルートレベルのエラーのみが含まれます。子孫フィールドのエラーが必要な場合は、ディープエラーメソッドを使用してください。単数形の getDeepError と getDeepErrorEntry は、エラーのある最初のフィールドで処理を停止します。これは、ネストされた構造に単一のメッセージを表示する場合に便利です。
変更状態の読み取り
import {
getDirtyInput,
getDirtyPaths,
isDirty,
pickDirty,
} from "@formisch/react";
// Raw dirty form input, or undefined when nothing is dirty
const dirtyInput = getDirtyInput(form);
// Paths of dirty fields (arrays are treated as atomic values)
const dirtyPaths = getDirtyPaths(form);
// Cheap boolean check when the dirty values are not needed
const hasChanges = isDirty(form);
// Boolean check scoped to a field and its descendants
const emailChanged = isDirty(form, { path: ["email"] });
// In a submit handler, keep transformed output only where fields are dirty
const dirtyOutput = pickDirty(form, { from: output });
getDirtyInput はフォームの生の入力値を返します。pickDirty は、指定した値にフォームの変更マスクを適用します。これは、検証および変換後の送信出力に便利です。
兄弟メソッドの isTouched、isEdited、isValid は、isDirty と同じパターンに従います。設定なしで呼び出すとフォーム全体を確認し、path を指定して呼び出すと特定のフィールドとその子孫を確認します。
値の設定
import { setInput, setErrors, reset } from "@formisch/react";
// Set field value (updates current input, not initial)
setInput(form, { path: ["email"], input: "[email protected]" });
// Set field errors manually
setErrors(form, { path: ["email"], errors: ["Email already taken"] });
// Clear errors
setErrors(form, { path: ["email"], errors: null });
// Reset entire form
reset(form);
// Reset a single field
reset(form, { path: ["email"] });
// Reset with new initial values
reset(form, {
initialInput: { email: "", password: "" },
});
// Reset but keep current input
reset(form, {
initialInput: newServerData,
keepInput: true,
});
reset は、keepTouched、keepEdited、keepErrors の各フラグも受け付けます。フォームレベルのリセットでは、さらに keepSubmitted も受け付けます。すべてのフラグのデフォルト値は false です。
フォームの制御
import { validate, focus, submit, handleSubmit } from "@formisch/react";
// Validate form manually (returns a Promise of a Valibot SafeParseResult)
const result = await validate(form);
if (result.success) {
console.log(result.output);
} else {
console.log(result.issues);
}
// Validate and focus first error field
await validate(form, { shouldFocus: true });
// Focus a specific field
focus(form, { path: ["email"] });
// Programmatically submit form
submit(form);
// Create submit handler for external buttons
const onExternalSubmit = handleSubmit(form, (output) => {
console.log(output);
});
submit には登録済みの DOM フォームが必要であり、@formisch/react-native からはエクスポートされません。React Native および <form> 要素のないレイアウトでは、代わりに handleSubmit が返す関数を呼び出してください。
フィールド配列
動的なフィールドリストには、配列操作メソッドとともに FieldArray を使用します。
フィールド配列ストアは、path、items(キーとして使用する安定したアイテム ID)、errors、isTouched、isEdited、isDirty、isValid を公開します。
スキーマ
const TodoSchema = v.object({
heading: v.pipe(v.string(), v.nonEmpty()),
todos: v.pipe(
v.array(
v.object({
label: v.pipe(v.string(), v.nonEmpty()),
deadline: v.pipe(v.string(), v.nonEmpty()),
}),
),
v.nonEmpty(),
v.maxLength(10),
),
});
React の例
import {
Field,
FieldArray,
Form,
useForm,
insert,
remove,
move,
swap,
} from "@formisch/react";
export default function TodoPage() {
const todoForm = useForm({
schema: TodoSchema,
initialInput: {
heading: "",
todos: [{ label: "", deadline: "" }],
},
});
return (
<Form of={todoForm} onSubmit={(output) => console.log(output)}>
<Field of={todoForm} path={["heading"]}>
{(field) => <input {...field.props} value={field.input} type="text" />}
</Field>
<FieldArray of={todoForm} path={["todos"]}>
{(fieldArray) => (
<div>
{fieldArray.items.map((item, index) => (
<div key={item}>
<Field of={todoForm} path={["todos", index, "label"]}>
{(field) => (
<input {...field.props} value={field.input} type="text" />
)}
</Field>
<Field of={todoForm} path={["todos", index, "deadline"]}>
{(field) => (
<input {...field.props} value={field.input} type="date" />
)}
</Field>
<button
type="button"
onClick={() =>
remove(todoForm, { path: ["todos"], at: index })
}
>
Delete
</button>
</div>
))}
{fieldArray.errors && <div>{fieldArray.errors[0]}</div>}
</div>
)}
</FieldArray>
<button
type="button"
onClick={() =>
insert(todoForm, {
path: ["todos"],
initialInput: { label: "", deadline: "" },
})
}
>
Add Todo
</button>
<button type="submit">Submit</button>
</Form>
);
}
配列メソッド
import { insert, remove, move, swap, replace } from "@formisch/react";
// Add item at end
insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } });
// Add item at specific index
insert(form, {
path: ["todos"],
at: 0,
initialInput: { label: "", deadline: "" },
});
// Remove item at index
remove(form, { path: ["todos"], at: index });
// Move item from one index to another
move(form, { path: ["todos"], from: 0, to: 3 });
// Swap two items
swap(form, { path: ["todos"], at: 0, and: 1 });
// Replace item at index
replace(form, {
path: ["todos"],
at: 0,
initialInput: { label: "New task", deadline: "2024-12-31" },
});
TypeScript との統合
型推論
Valibot スキーマから型が自動的に推論されます。
const LoginSchema = v.object({
email: v.pipe(v.string(), v.email()),
password: v.pipe(v.string(), v.minLength(8)),
});
const form = useForm({ schema: LoginSchema });
// form is FormStore<typeof LoginSchema>
// Submit handler receives typed output
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
output.email; // ✓ string
output.password; // ✓ string
output.username; // ✗ TypeScript error
};
入力型と出力型
変換を含むスキーマでは、入力型と出力型が異なります。
const ProfileSchema = v.object({
age: v.pipe(
v.string(), // Input: string
v.transform((input) => Number(input)), // Output: number
v.number(),
),
birthDate: v.pipe(
v.string(), // Input: string
v.transform((input) => new Date(input)), // Output: Date
v.date(),
),
});
// In Field: field.input is string
// In onSubmit: output.age is number, output.birthDate is Date
型安全な Props
適切な型を指定して、フォームを子コンポーネントに渡します。
import { Form, type FormStore, useForm } from "@formisch/react";
export default function LoginPage() {
const loginForm = useForm({ schema: LoginSchema });
return <FormContent of={loginForm} />;
}
type FormContentProps = {
of: FormStore<typeof LoginSchema>;
};
function FormContent({ of }: FormContentProps) {
return (
<Form of={of} onSubmit={(output) => console.log(output)}>
{/* ... */}
</Form>
);
}
ジェネリックなフィールドコンポーネント
適切な型を指定して、再利用可能なフィールドコンポーネントを作成します。
import { useField, type FormStore } from "@formisch/react";
import * as v from "valibot";
type EmailInputProps = {
of: FormStore<v.GenericSchema<{ email: string }>>;
};
function EmailInput({ of }: EmailInputProps) {
const field = useField(of, { path: ["email"] });
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}
v.GenericSchema<{ email: string }> の制約は、スキーマに string 型の email フィールドを含む任意のフォームを受け付けます。TypeScript はコンパイル時に不一致を検出します。
利用可能な型
import type {
FormStore, // Form store type
FieldStore, // Field store type
FieldArrayStore, // Field array store type
SubmitHandler, // Submit handler function type
ValidPath, // Valid field path type
ValidArrayPath, // Valid array field path type
Schema, // Base schema type from Valibot
} from "@formisch/react";
検証のタイミング
validate オプション
最初の検証をいつ行うかを制御します。
| 値 | 説明 |
|---|---|
'initial' | フォームの作成直後に検証する |
'touch' | フィールドが初めてフォーカスされたときに検証する |
'input' | 入力イベントごとに検証する |
'change' | 変更イベント時(値が確定したとき)に検証する |
'blur' | フィールドのフォーカスが外れたときに検証する |
'submit' | フォームの送信時のみ検証する(デフォルト) |
revalidate オプション
すでにエラーがある場合、またはフォームを送信した後に、フィールドを再度検証するタイミングを制御します。
| 値 | 説明 |
|---|---|
'touch' | フィールドに初めてフォーカスしたときに再検証します |
'input' | 入力イベントごとに再検証します(デフォルト) |
'change' | 変更イベント時に再検証します(値が確定したとき) |
'blur' | フィールドからフォーカスが外れたときに再検証します |
'submit' | フォームの送信時のみ再検証します |
特殊な入力
セレクト(単一)
<Field of={form} path={["framework"]}>
{(field) => (
<select {...field.props} value={field.input ?? ""}>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
)}
</Field>
セレクト(複数)
<Field of={form} path={["frameworks"]}>
{(field) => (
<select {...field.props} value={field.input ?? []} multiple>
{options.map(({ label, value }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
)}
</Field>
チェックボックス
<Field of={form} path={["acceptTerms"]}>
{(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>
ファイル入力
ファイル入力は制御できません。周囲の UI で処理してください。
<Field of={form} path={["avatar"]}>
{(field) => (
<div>
<input {...field.props} type="file" />
{field.input && <span>{field.input.name}</span>}
</div>
)}
</Field>
useField フック
複雑なフィールドコンポーネントでは、Field コンポーネントの代わりに useField フックを使用します。
import { useField } from "@formisch/react";
import { useEffect } from "react";
function EmailInput({ form }) {
const field = useField(form, { path: ["email"] });
// Access field state in component logic
useEffect(() => {
if (field.errors) {
console.log("Email has errors:", field.errors);
}
}, [field.errors]);
return (
<div>
<input {...field.props} value={field.input} type="email" />
{field.errors && <div>{field.errors[0]}</div>}
</div>
);
}
使い分け:
Fieldコンポーネント — 同じコンポーネント内に複数のフィールドがある場合useFieldフック — コンポーネントのロジックにアクセスする単一のフィールド
useFieldArray フックは、FieldArray コンポーネントに相当します。Angular では、injectField 関数と injectFieldArray 関数、または *formischField ディレクティブと *formischFieldArray ディレクティブを使用します。
コンポーネントライブラリの使用
内部のネイティブ要素を公開しないコンポーネントライブラリを使用する場合、field.props を直接展開できません。その代わり、field.onChange(React と React Native)、field.onInput(Solid、Svelte、Preact、Qwik)、field.setInput(Angular)でプログラムから値を更新するか、field.input(Vue)に値を代入します。
import { DatePicker } from "some-component-library";
<Field of={form} path={["date"]}>
{(field) => (
<DatePicker
value={field.input}
onChange={(newDate) => field.onChange(newDate)}
/>
)}
</Field>;
これらのセッターは、ネイティブ入力と同様にフィールドの値を更新し、検証を実行します。
次のような場合に便利です。
- コンポーネントライブラリ — ネイティブ要素をラップしながら、それらを公開しないもの
- 日付ピッカー、リッチテキストエディター、カラーピッカーなどの複雑なカスタム入力
非同期送信
import { setErrors, useForm } from "@formisch/react";
import type { SubmitHandler } from "@formisch/react";
const loginForm = useForm({ schema: LoginSchema });
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (output) => {
try {
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(output),
});
if (!response.ok) {
// Set server-side errors
const data = await response.json();
setErrors(loginForm, { path: ["email"], errors: [data.error] });
}
} catch (error) {
console.error("Submission failed:", error);
}
};
非同期ハンドラーが解決するまで、フォームの isSubmitting 状態は true のままです。ハンドラーが例外をスローした場合、Formisch はそのエラーを捕捉し、メッセージを form.errors のフォームルートレベルのエラーとして設定します。
よくあるパターン
読み込み状態
<button type="submit" disabled={form.isSubmitting}>
{form.isSubmitting ? "Submitting..." : "Submit"}
</button>
Enter キーで送信
Formisch はネイティブの <form> 要素を通じてこれを自動的に処理します。
成功後にリセット
const handleSubmit: SubmitHandler<typeof Schema> = async (output) => {
await saveData(output);
// Full reset to initial state
reset(form);
// Or reset but keep current input values
reset(form, { keepInput: true });
};
サーバーデータとの同期
サーバーデータが変更されたときは、ユーザーの編集内容を失わずにベースラインを更新します。
// After refetching data from server
reset(form, {
initialInput: newServerData,
keepInput: true, // Keep user's current edits
keepTouched: true, // Keep touched state (optional)
});
条件付きフィールド
<Form of={form} onSubmit={handleSubmit}>
<Field of={form} path={["hasAccount"]}>
{(field) => (
<input {...field.props} type="checkbox" checked={field.input} />
)}
</Field>
{getInput(form, { path: ["hasAccount"] }) && (
<Field of={form} path={["accountId"]}>
{(field) => <input {...field.props} value={field.input} />}
</Field>
)}
</Form>
React では、レンダリング中に getInput を呼び出すとリアクティブになります。これは、useForm と useField が、それらを呼び出すコンポーネントでシグナルの追跡を有効にするためです。他のフレームワークでは、それぞれのリアクティブスコープによって読み取りが追跡されます。
追加リソース
- Formisch ドキュメント
- Formisch コーディングエージェントガイド
- Formisch MCP サーバー:
https://formisch.dev/mcp(search_docs、get_doc、list_docs) - エージェント向けの Markdown を取得するには、任意のドキュメント URL の末尾に
.mdを追加してください。または、フレームワーク固有のインデックスとしてhttps://formisch.dev/llms-{framework}.txtを使用してください - Formisch GitHub
- Valibot ドキュメント