falcondev-form について
`@vue/reactivity` を基盤とする、フレームワーク非依存のリアクティブなフォーム状態ライブラリです。Standard Schema による検証と遅延フィールドアクセサーを備え、React と Vue のバインディングを提供します。
主な機能
- 01Standard Schema による検証
- 02リアクティブなフォームとフィールド状態
- 03遅延型付きフィールドアクセサー
- 04Nullable な編集と検証後の送信
- 05React と Vue のバインディング
活用シーン
- 01Schema 検証付き React フォームの構築
- 02Schema 検証付き Vue フォームの構築
- 03ネストしたフィールドや配列フィールドの管理
- 04送信前の Nullable な編集の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ falcondev-oss/ skills --skill falcondev-formインストール後、Claude Code で/falcondev-formを入力して読み込まれたか確認します。
同梱ファイルreference/frameworks.mdreference/patterns.md
詳細
@falcondev-oss/form
@vue/reactivity 上に構築された、フレームワークに依存しないフォーム状態ライブラリです。Standard Schema(zod v4、arktype など)とリアクティブな sourceValues を渡すと、フィールド アクセサーのツリーを持つリアクティブなフォーム ハンドルを返します。
公開されているパッケージは 3 つです。実行環境に合ったものを選び、アプリケーションコードから -core を直接インポートしないでください。
| パッケージ | エントリーポイント | 追加機能 |
|---|---|---|
@falcondev-oss/form-core | useFormCore、すべての型、/reactive ヘルパー | エンジン。React/Vue 以外でのみ直接使用します |
@falcondev-oss/form-react | useForm、useField、FormFieldMemo | React の再レンダリング連携、field.model = {value,onUpdate} |
@falcondev-oss/form-vue | useForm、useFormHandles | 書き込み可能な算出プロパティ field.model(v-model) |
フレームワーク用パッケージの useForm は useFormCore をラップします。オプションもハンドルも同じです。以下はフィールドのバインディングと再レンダリングを除き、すべてのフレームワークで共通です(reference/frameworks.md を参照)。
すべてを説明する 2 つの考え方
-
アクセサーは遅延評価されるパスで、
$use()が実体化します。form.fields.address.cityは型付きパスのプロキシにすぎず、フィールドオブジェクトはまだ存在しません。.$use()を呼び出すと、実際のリアクティブなFormFieldが作成され、キャッシュされます。ドットまたは.at()でたどり、バインドする末端で.$use()を呼び出してください。 -
フォームデータは
NullableDeepです。 編集中は、すべての値がnullになりえます。入力途中のフォームでは、あらゆる場所に null が存在します。そのため、form.data、field.value、アクセサーの型は、スキーマの 入力型を深く nullable にしたものです(オブジェクト →T | null、配列 →(T|null)[] | null)。検証済みの 出力型(null なし)が現れるのはsubmit({ values })のみです。UI は nullable 型に合わせて記述し、クリーンなデータはsubmitで得られるものとして扱ってください。
基本的な使い方
import { useForm } from '@falcondev-oss/form-vue' // or -react
import z from 'zod'
const form = useForm({
schema: z.object({ name: z.string(), age: z.number() }),
sourceValues, // initial data (NullableDeep); a value, getter, or ref — React needs a stable reference
async submit({ values }) {
// `values` is validated OUTPUT: { name: string, age: number }
await api.save(values)
// return nothing → { success: true }; return { success: false } to keep form dirty
},
})
const nameField = form.fields.name.$use()
nameField.handleChange('Jane') // programmatic write (validates, marks dirty)
// or write straight through the reactive data:
form.data.name = 'Jane' // equivalent effect
await form.submit() // validates whole schema, runs submit if valid
useForm(options)
schema— Standard JSON Schema も公開する Standard Schema(zod v4、arktype など)です。JSON Schema はfield.schemaのメタデータと必須項目の判定に使われます。JSON で表現できない型(Date、bigint、Map など)には codec/transform が必要です。指定しないとメタデータの抽出は警告なしに機能低下します(コンソールに警告が表示されます)。sourceValues— 初期のNullableDeepデータです。値、getter() => data、または(Vue の場合)refを指定します。React では安定した参照である必要があります。詳細は後述します。undefinedの場合、解決されるまでフォームは pending 状態になります(isLoading/field.isPendingはtrue、dataはundefinedで、書き込みは無視されます)。その後値が変わったとき、フォームが dirty でなければ 新しい値でリセットされ、dirty であれば警告を出して更新をスキップします(submit 中を除く)。submit({ values })—asyncで、検証済みの出力を受け取ります。void/{success:true}を返すと、フォームは pristine としてマークされます。{success:false}を返すと、dirty のままになります。フォームの状態が確定する前に完了する必要がある処理は、すべてawaitしてください。特にキャッシュやクエリの無効化は重要です。フォームが pristine としてマークされる前に最新データがsourceValuesに反映される必要があります(無効化をawaitしないと、古いデータのままフォームの状態が確定します)。TanStack Query を使う場合は、submit内でqueryClient.invalidateQueries(...)を直接awaitするか、mutation のonSuccessで無効化し、submitでawait mutateAsync(...)を呼び出してください(mutation を await するとonSuccessも await されます)。disabled?—boolean | Ref | getterです。すべてのフィールドでhandleChange/handleBlur/resetをブロックします。hooks?— ライフサイクルフックです(reference/patterns.mdを参照)。
sourceValues の記述(この方法で行う)
順序どおりに3つの分岐を設けます。読み込み中 → 既存のエンティティ → 空のデフォルト値です。Vue ではゲッターとして、React では useMemo でメモ化した値として渡します。
// Vue — getter, tracked reactively
useForm({
schema,
sourceValues: () => {
if (isLoading.value) return undefined // 1. source still loading → form is pending
if (entity.value) return entity.value // 2. edit: return the whole entity object
return { name: null, age: null } // 3. create: blank defaults
},
async submit({ values }) { /* … */ },
})
// React — same branches, wrapped in useMemo
const sourceValues = useMemo(() => {
if (isLoading) return undefined
if (entity) return entity
return { name: null, age: null }
}, [isLoading, entity])
useForm({ schema, sourceValues, async submit({ values }) { /* … */ } })
React では、sourceValues は安定した参照でなければなりません。 useForm の呼び出しでオブジェクト(またはゲッター)を直接インラインで指定しないでください。React はレンダーのたびにコンポーネント本体を再実行するため、新しいオブジェクトが作られると、レンダーのたびにフォームの初期値が設定されます。その結果、変更のないフォームはリセットされ、変更済みのフォームでは警告が表示されます。useMemo でメモ化するか、定数をモジュールスコープに置くか、クエリデータを直接渡してください(sourceValues: query.data — これはすでに安定しています)。ここではゲッターは役に立たないどころか、かえって問題になります。アダプターはマウント時にゲッターを一度だけ取得するため、React の状態や props に基づくゲッターは初回レンダーのクロージャーで固定され、更新を一切反映できません。
- ソースの読み込み中は
undefinedを返します(例:編集対象のエンティティの取得が保留中の場合)。これにより、後で上書きする必要のある誤った、または空の形状で初期値を設定するのではなく、フォームを保留状態にできます。 - 既存のエンティティをオブジェクト全体として返します。キーごとにオプショナルチェイニングを使った
{ name: entity?.name, age: entity?.age, … }を組み立てないでください。エンティティがスキーマと一致しない場合は、差異のあるフィールドだけをスプレッドして上書きします。return { ...entity, tags: entity.tags ?? [] } - 意味のあるデフォルト値がないフィールドには
nullを使います。型が文字列や数値だからといって、''や0を使わないでください。NullableDeepにより、すべてのフィールドでnullが有効になります。空文字列は「ユーザーが何も入力しなかった」という実際の値であり、「未操作」とは異なります。
フォームハンドル
| メンバー | 型 | 説明 |
|---|---|---|
form.data | NullableDeep<T> | undefined | リアクティブです。直接読み取り、書き込みます(form.data.x = …)。保留中は undefined です。 |
form.fields | アクセサーツリー | たどってから .$use() を呼び出します |
form.isDirty | boolean | 変更があったかどうか(編集回数 ≠ 0)。submit 成功時または reset によりリセットされます |
form.isChanged | boolean | 現在のデータと sourceValues をディープイコールで比較します(元の値に戻すと false になります) |
form.isLoading | boolean | 送信中またはソース値が保留中 |
form.isDisabled | boolean | 読み込み中、または disabled オプションが指定されている場合 |
form.errors | Issue[] | undefined | すべての検証エラー。ない場合は undefined |
form.submit() | () => Promise<{success:boolean}> | すべてを検証し、submit を実行します |
form.reset() | () => void | sourceValues の値に戻します |
form.hooks | フック対応 | hook/hookOnce/addHooks |
フィールドアクセサー
form.fields.user.email.$use() // nested object
form.fields.tags.at(0).$use() // array item (negative index ok: .at(-1))
form.fields.tags.$use() // the array field itself
for (const item of form.fields.tags) item.$use() // iterate (reactive)
form.fields.tags.delete(field.key) // remove array item by its key (see below)
form.fields.$use() // root field = whole form value
form.fields['a.b'].$use() // keys containing dots work (auto-escaped)
配列やインデックスが存在する前でも、.at(i) アクセサーは存在します。データが届くまでは .$use().value は null です。.delete(key) は配列アイテムのキー(items[2])を対象にする必要があります。ネストされたプロパティを対象にすると、エラーが発生します。
FormField($use() の戻り値)
| メンバー | 説明 |
|---|---|
field.value | フィールドの NullableDeep 値を持つ読み取り専用 ref です。ネストされたオブジェクトのプロパティは引き続き書き込み可能で(書き込みは form.data に反映されます)、配列には .push を使えます。 |
field.handleChange(v) | 値を設定し、検証を行い、変更済みとしてマークして、フィールド変更フックを発火します |
field.handleBlur() | フィールドが編集されている場合に検証します(入力の blur 時に使用します) |
field.reset() | このフィールドをソース値に戻します |
field.errors | string[] | undefined — このフィールド(およびネストされた未割り当てのエラー)のメッセージ |
field.schema | SchemaMeta — スキーマから取得した required、title、min/maxLength、minimum/maximum など(パターンを参照) |
field.disabled / field.isPending | フォームの状態を反映します |
field.isDirty / field.isChanged | フィールド単位の値で、フォームと同じ意味です |
field.path | ドット/角括弧形式のパス文字列 |
field.key | 安定した一意の ID(path@timestamp-rand)。リストの :key および array.delete() に使用します |
field.model | フレームワークバインディング — Vue:書き込み可能な computed(v-model)。React:{ value, onUpdate }。フレームワークを参照してください。 |
field.$() | フィールドからアクセサーツリーを取得します($use したフィールドの子要素にアクセスするため) |
2 つの書き込み方法(同等)
field.handleChange(newValue)— 明示的な方法で、入力ハンドラーが呼び出します。form.data.path = newValue— 直接リアクティブに変更する方法です。on-changeが変更を検知し、同じバリデーションを実行します。
どちらもフォームを変更済みにし、再バリデーションを実行します。コンポーネントでは handleChange/model を使用してください。テストやエフェクトでは直接書き込むと便利です。
バリデーションが実行されるタイミング
スキーマ全体を検証し(フィールドごとの検証ではありません)、その後、各フィールドのパスに該当する問題を抽出します。
- 送信時 — 常に実行します。
- 変更時 — フィールドにすでにエラーがある場合のみ実行します(エラーを修正すると、その場で消去されます)。
- フォーカス離脱時 — フィールドが編集済みの場合に実行します。
isLoading 中はエラーが消去されます。また、フィールドは、まだ $use されていないネストしたパスのバリデーション問題も表示します。
注意点
- 読み書き時は nullable、送信時は non-null です。 UI で
field.valueが non-null だと仮定しないでください。 - バインドには
$use()が必要です。 アクセサーだけでは何も動作しないパスです。React では、$use()によってuseFormを所有するコンポーネントが再レンダリングされます。フィールドの prop を読み取る子コンポーネントを再レンダリングするには、useField(field)またはFormFieldMemoが必要です(フレームワークを参照してください)。 sourceValuesは変更済みのフォームを上書きしません(仕様です)。強制的に反映する必要がある場合は、先にリセットしてください。delete(key)には配列項目のキーが必要です。handleChangeで配列を置き換えるとfield.keyが変わります(キャッシュがクリアされます)。- React/Vue アプリでは
-coreからインポートしないでください。再レンダリング/modelとの連携が失われます。
詳細リファレンス
- フレームワークごとの詳細(React の再レンダリング/
useField/FormFieldMemo、Vue のv-model/useFormHandles、core の単独使用)→reference/frameworks.mdを参照してください。 - オプション機能 — 判別可能なユニオン(
$use({discriminator}))、値の変換($use({translate}))、field.schemaのメタデータ、フック →reference/patterns.mdを参照してください。