formisch

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Formisch

Formisch

This skill helps AI agents work effectively with Formisch, the schema-based, headless form library for modern frameworks.
本技能可帮助AI Agent高效使用Formisch——一款基于Schema的无UI表单库,适用于各类现代前端框架。

When to Use This Skill

适用场景

  • When the user asks about form handling with Formisch
  • When managing form state and validation
  • When working with React, Vue, Solid, Preact, Svelte, or Qwik forms
  • When integrating Valibot schemas with forms
  • 当用户咨询如何使用Formisch处理表单时
  • 管理表单状态与验证逻辑时
  • 开发React、Vue、Solid、Preact、Svelte或Qwik框架下的表单时
  • 集成Valibot Schema与表单时

Introduction

简介

Formisch is a schema-based, headless form library that works across multiple frameworks. Key highlights:
  • Small bundle size — Starting at ~2.5 kB
  • Schema-based validation — Uses Valibot for type-safe validation
  • Headless design — You control the UI completely
  • Type safety — Full TypeScript support with autocompletion
  • Framework-native — Native performance for each supported framework
Formisch是一款跨多框架的Schema优先无UI表单库,核心特性如下:
  • 体积小巧 —— 最小仅约2.5 kB
  • Schema驱动验证 —— 基于Valibot实现类型安全的验证
  • 无UI设计 —— 完全由开发者控制表单UI
  • 类型安全 —— 完整支持TypeScript与自动补全
  • 框架原生性能 —— 为每个支持的框架提供原生级性能

Supported Frameworks

支持的框架

FrameworkPackageHook/Primitive
React
@formisch/react
useForm
Vue
@formisch/vue
useForm
SolidJS
@formisch/solid
createForm
Preact
@formisch/preact
useForm
Svelte
@formisch/svelte
createForm
Qwik
@formisch/qwik
useForm$
框架包名Hook/基础组件
React
@formisch/react
useForm
Vue
@formisch/vue
useForm
SolidJS
@formisch/solid
createForm
Preact
@formisch/preact
useForm
Svelte
@formisch/svelte
createForm
Qwik
@formisch/qwik
useForm$

Installation

安装步骤

1. Install Valibot (peer dependency)

1. 安装Valibot(依赖包)

bash
npm install valibot
bash
npm install valibot

2. Install Formisch for your framework

2. 安装对应框架的Formisch包

bash
npm install @formisch/react   # React
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
bash
npm install @formisch/react   # React
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

Core Concepts

核心概念

Schema-First Design

Schema优先设计

Every form starts with a Valibot schema. Types are automatically inferred from the schema.
ts
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."),
  ),
});
每个表单都从Valibot Schema开始,类型会自动从Schema中推导。
ts
import * as v from "valibot";

const LoginSchema = v.object({
  email: v.pipe(
    v.string("请输入您的邮箱。"),
    v.nonEmpty("请输入您的邮箱。"),
    v.email("邮箱格式不正确。"),
  ),
  password: v.pipe(
    v.string("请输入您的密码。"),
    v.nonEmpty("请输入您的密码。"),
    v.minLength(8, "密码长度至少为8位。"),
  ),
});

Form Store

表单存储(Form Store)

The form store manages all form state. Access it via the framework-specific hook/primitive.
Form Store Properties:
  • isSubmitting
    — Form is currently being submitted
  • isSubmitted
    — Form has been successfully submitted
  • isValidating
    — Validation is in progress
  • isTouched
    — At least one field has been touched
  • isDirty
    — At least one field differs from initial value
  • isValid
    — All fields pass validation
  • errors
    — Root-level validation errors
表单存储管理所有表单状态,可通过框架专属的Hook/基础组件访问。
表单存储属性:
  • isSubmitting
    —— 表单正在提交中
  • isSubmitted
    —— 表单已成功提交
  • isValidating
    —— 正在执行验证
  • isTouched
    —— 至少有一个字段已被触碰
  • isDirty
    —— 至少有一个字段值与初始值不同
  • isValid
    —— 所有字段均通过验证
  • errors
    —— 根级别的验证错误

Field Store

字段存储(Field Store)

Each field has its own reactive store with:
  • path
    — Path array to the field
  • input
    — Current field value
  • errors
    — Field-specific errors
  • isTouched
    — Field has been focused and blurred
  • isDirty
    — Field value differs from initial value
  • isValid
    — Field passes validation
  • props
    — Props to spread onto input elements
  • onChange
    (React) /
    onInput
    (other frameworks) — Sets the field input value programmatically. Use this when the field cannot be connected to a native HTML element.
每个字段都有独立的响应式存储,包含以下属性:
  • path
    —— 字段的路径数组
  • input
    —— 当前字段值
  • errors
    —— 字段专属的错误信息
  • isTouched
    —— 字段已被聚焦并失焦
  • isDirty
    —— 字段值与初始值不同
  • isValid
    —— 字段通过验证
  • props
    —— 可直接扩散到输入元素的属性
  • onChange
    (React) /
    onInput
    (其他框架) —— 以编程方式设置字段值,适用于无法关联原生HTML元素的字段

Dirty Tracking

脏值追踪

Formisch tracks two inputs per field:
  • Initial input — Baseline for dirty tracking (server state)
  • Current input — What the user is editing (client state)
isDirty
becomes
true
when current input differs from initial input.
Formisch为每个字段追踪两个输入值:
  • 初始输入值 —— 脏值追踪的基准(服务端状态)
  • 当前输入值 —— 用户正在编辑的值(客户端状态)
当当前输入值与初始输入值不同时,
isDirty
会变为
true

Framework Examples

框架示例

React Example

React 示例

tsx
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>
  );
}
tsx
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 ? "提交中..." : "登录"}
      </button>
    </Form>
  );
}

Vue Example

Vue 示例

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-bind="field.props" v-model="field.input" type="email" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <Field :of="loginForm" :path="['password']" v-slot="field">
      <div>
        <input v-bind="field.props" v-model="field.input" type="password" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <button type="submit">Login</button>
  </Form>
</template>
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-bind="field.props" v-model="field.input" type="email" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <Field :of="loginForm" :path="['password']" v-slot="field">
      <div>
        <input v-bind="field.props" v-model="field.input" type="password" />
        <div v-if="field.errors">{{ field.errors[0] }}</div>
      </div>
    </Field>
    <button type="submit">登录</button>
  </Form>
</template>

SolidJS Example

SolidJS 示例

tsx
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>
  );
}
tsx
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">登录</button>
    </Form>
  );
}

Svelte Example

Svelte 示例

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>
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">登录</button>
</Form>

Qwik Example

Qwik 示例

tsx
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>
  );
});
tsx
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">登录</button>
    </Form>
  );
});

Form Configuration

表单配置

ts
const form = useForm({
  // Required: Valibot schema
  schema: MySchema,

  // Optional: Initial values (partial allowed)
  initialInput: {
    email: "user@example.com",
  },

  // Optional: When first validation occurs
  // Options: 'initial' | 'blur' | 'input' | 'submit' (default)
  validate: "submit",

  // Optional: When revalidation occurs after first validation
  // Options: 'blur' | 'input' (default) | 'submit'
  revalidate: "input",
});
ts
const form = useForm({
  // 必填:Valibot Schema
  schema: MySchema,

  // 可选:初始值(支持部分赋值)
  initialInput: {
    email: "user@example.com",
  },

  // 可选:首次验证触发时机
  // 选项:'initial' | 'blur' | 'input' | 'submit'(默认)
  validate: "submit",

  // 可选:首次验证后的重验证触发时机
  // 选项:'blur' | 'input'(默认) | 'submit'
  revalidate: "input",
});

Field Paths

字段路径

Paths are type-safe arrays that reference fields in your schema.
tsx
// 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} />
))}
路径是类型安全的数组,用于引用Schema中的字段。
tsx
// 顶层字段
<Field of={form} path={['email']} />

// 嵌套字段(Schema: { user: { email: string } })
<Field of={form} path={['user', 'email']} />

// 数组项字段(Schema: { todos: [{ label: string }] })
<Field of={form} path={['todos', 0, 'label']} />

// 动态数组索引
{items.map((item, index) => (
  <Field of={form} path={['todos', index, 'label']} key={item} />
))}

Form Methods

表单方法

All methods follow a consistent API pattern:
  • First parameter: Form store
  • Second parameter: Config object
所有方法遵循一致的API模式:
  • 第一个参数:表单存储
  • 第二个参数:配置对象

Reading Values

读取值

ts
import { getInput, getErrors, getAllErrors } 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
const allErrors = getAllErrors(form);
ts
import { getInput, getErrors, getAllErrors } from "@formisch/react";

// 获取单个字段值
const email = getInput(form, { path: ["email"] });

// 获取整个表单的输入值
const allInputs = getInput(form);

// 获取单个字段的错误信息
const emailErrors = getErrors(form, { path: ["email"] });

// 获取所有字段的错误信息
const allErrors = getAllErrors(form);

Setting Values

设置值

ts
import { setInput, setErrors, reset } from "@formisch/react";

// Set field value (updates current input, not initial)
setInput(form, { path: ["email"], input: "new@example.com" });

// 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 with new initial values
reset(form, {
  initialInput: { email: "", password: "" },
});

// Reset but keep current input
reset(form, {
  initialInput: newServerData,
  keepInput: true,
});
ts
import { setInput, setErrors, reset } from "@formisch/react";

// 设置单个字段值(更新当前输入值,不修改初始值)
setInput(form, { path: ["email"], input: "new@example.com" });

// 手动设置字段错误
setErrors(form, { path: ["email"], errors: ["该邮箱已被注册"] });

// 清除错误
setErrors(form, { path: ["email"], errors: null });

// 重置整个表单
reset(form);

// 使用新的初始值重置表单
reset(form, {
  initialInput: { email: "", password: "" },
});

// 重置表单但保留当前输入值
reset(form, {
  initialInput: newServerData,
  keepInput: true,
});

Form Control

表单控制

ts
import { validate, focus, submit, handleSubmit } from "@formisch/react";

// Validate form manually
const isValid = await validate(form);

// 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);
});
ts
import { validate, focus, submit, handleSubmit } from "@formisch/react";

// 手动验证表单
const isValid = await validate(form);

// 验证并聚焦第一个错误字段
await validate(form, { shouldFocus: true });

// 聚焦指定字段
focus(form, { path: ["email"] });

// 以编程方式提交表单
submit(form);

// 为外部按钮创建提交处理器
const onExternalSubmit = handleSubmit(form, (output) => {
  console.log(output);
});

Field Arrays

字段数组

For dynamic lists of fields, use
FieldArray
with array manipulation methods.
对于动态字段列表,可使用
FieldArray
结合数组操作方法。

Schema

Schema

ts
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),
  ),
});
ts
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 Example

React 示例

tsx
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>
  );
}
tsx
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 })
                  }
                >
                  删除
                </button>
              </div>
            ))}
            {fieldArray.errors && <div>{fieldArray.errors[0]}</div>}
          </div>
        )}
      </FieldArray>

      <button
        type="button"
        onClick={() =>
          insert(todoForm, {
            path: ["todos"],
            initialInput: { label: "", deadline: "" },
          })
        }
      >
        添加待办
      </button>

      <button type="submit">提交</button>
    </Form>
  );
}

Array Methods

数组操作方法

ts
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" },
});
ts
import { insert, remove, move, swap, replace } from "@formisch/react";

// 在末尾添加项
insert(form, { path: ["todos"], initialInput: { label: "", deadline: "" } });

// 在指定索引处添加项
insert(form, {
  path: ["todos"],
  at: 0,
  initialInput: { label: "", deadline: "" },
});

// 删除指定索引处的项
remove(form, { path: ["todos"], at: index });

// 将项从一个索引移动到另一个索引
move(form, { path: ["todos"], from: 0, to: 3 });

// 交换两个项的位置
swap(form, { path: ["todos"], at: 0, and: 1 });

// 替换指定索引处的项
replace(form, {
  path: ["todos"],
  at: 0,
  initialInput: { label: "新任务", deadline: "2024-12-31" },
});

TypeScript Integration

TypeScript 集成

Type Inference

类型推导

Types are automatically inferred from your Valibot schema:
ts
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
};
类型会自动从Valibot Schema中推导:
ts
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 类型为 FormStore<typeof LoginSchema>

// 提交处理器接收类型化的输出
const handleSubmit: SubmitHandler<typeof LoginSchema> = (output) => {
  output.email; // ✓ string
  output.password; // ✓ string
  output.username; // ✗ TypeScript 错误
};

Input vs Output Types

输入与输出类型

Schemas with transformations have different input and output types:
ts
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
带有转换逻辑的Schema会有不同的输入和输出类型:
ts
const ProfileSchema = v.object({
  age: v.pipe(
    v.string(), // 输入类型: string
    v.transform((input) => Number(input)), // 输出类型: number
    v.number(),
  ),
  birthDate: v.pipe(
    v.string(), // 输入类型: string
    v.transform((input) => new Date(input)), // 输出类型: Date
    v.date(),
  ),
});

// 在Field中: field.input 类型为 string
// 在onSubmit中: output.age 类型为 number, output.birthDate 类型为 Date

Type-Safe Props

类型安全的属性

Pass forms to child components with proper typing:
tsx
import type { FormStore } from "@formisch/react";

type FormContentProps = {
  of: FormStore<typeof LoginSchema>;
};

function FormContent({ of }: FormContentProps) {
  return (
    <Form of={of} onSubmit={(output) => console.log(output)}>
      {/* ... */}
    </Form>
  );
}
将表单传递给子组件时保持正确的类型:
tsx
import type { FormStore } from "@formisch/react";

type FormContentProps = {
  of: FormStore<typeof LoginSchema>;
};

function FormContent({ of }: FormContentProps) {
  return (
    <Form of={of} onSubmit={(output) => console.log(output)}>
      {/* ... */}
    </Form>
  );
}

Generic Field Components

通用字段组件

Create reusable field components with proper typing:
tsx
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>
  );
}
创建带有正确类型的可复用字段组件:
tsx
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>
  );
}

Available Types

可用类型

ts
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";
ts
import type {
  FormStore, // 表单存储类型
  FieldStore, // 字段存储类型
  FieldArrayStore, // 字段数组存储类型
  SubmitHandler, // 提交处理器函数类型
  ValidPath, // 有效的字段路径类型
  ValidArrayPath, // 有效的数组字段路径类型
  Schema, // Valibot的基础Schema类型
} from "@formisch/react";

Validation Timing

验证时机

validate Option

validate 选项

Controls when the first validation occurs:
ValueDescription
'initial'
Validate immediately on form creation
'blur'
Validate when field loses focus
'input'
Validate on every input change
'submit'
Validate only on form submission (default)
控制首次验证的触发时机:
描述
'initial'
在表单创建后立即验证
'blur'
当字段失焦时验证
'input'
在每次输入变化时验证
'submit'
仅在表单提交时验证(默认值)

revalidate Option

revalidate 选项

Controls when validation runs after the first validation:
ValueDescription
'blur'
Revalidate when field loses focus
'input'
Revalidate on every input change (default)
'submit'
Revalidate only on form submission
控制首次验证之后的重验证触发时机:
描述
'blur'
当字段失焦时重验证
'input'
在每次输入变化时重验证(默认值)
'submit'
仅在表单提交时重验证

Special Inputs

特殊输入组件

Select (Single)

单选下拉框

tsx
<Field of={form} path={["framework"]}>
  {(field) => (
    <select {...field.props}>
      {options.map(({ label, value }) => (
        <option key={value} value={value} selected={field.input === value}>
          {label}
        </option>
      ))}
    </select>
  )}
</Field>
tsx
<Field of={form} path={["framework"]}>
  {(field) => (
    <select {...field.props}>
      {options.map(({ label, value }) => (
        <option key={value} value={value} selected={field.input === value}>
          {label}
        </option>
      ))}
    </select>
  )}
</Field>

Select (Multiple)

多选下拉框

tsx
<Field of={form} path={["frameworks"]}>
  {(field) => (
    <select {...field.props} multiple>
      {options.map(({ label, value }) => (
        <option
          key={value}
          value={value}
          selected={field.input?.includes(value)}
        >
          {label}
        </option>
      ))}
    </select>
  )}
</Field>
tsx
<Field of={form} path={["frameworks"]}>
  {(field) => (
    <select {...field.props} multiple>
      {options.map(({ label, value }) => (
        <option
          key={value}
          value={value}
          selected={field.input?.includes(value)}
        >
          {label}
        </option>
      ))}
    </select>
  )}
</Field>

Checkbox

复选框

tsx
<Field of={form} path={["acceptTerms"]}>
  {(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>
tsx
<Field of={form} path={["acceptTerms"]}>
  {(field) => <input {...field.props} type="checkbox" checked={field.input} />}
</Field>

File Input

文件输入

File inputs cannot be controlled. Handle via UI around them:
tsx
<Field of={form} path={["avatar"]}>
  {(field) => (
    <div>
      <input {...field.props} type="file" />
      {field.input && <span>{field.input.name}</span>}
    </div>
  )}
</Field>
文件输入无法被控制,可通过周边UI处理:
tsx
<Field of={form} path={["avatar"]}>
  {(field) => (
    <div>
      <input {...field.props} type="file" />
      {field.input && <span>{field.input.name}</span>}
    </div>
  )}
</Field>

useField Hook

useField Hook

For complex field components, use the
useField
hook instead of the
Field
component:
tsx
import { useField } from "@formisch/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>
  );
}
When to use which:
  • Field
    component
    — Multiple fields in the same component
  • useField
    hook
    — Single field with component logic access
对于复杂的字段组件,可使用
useField
Hook替代
Field
组件:
tsx
import { useField } from "@formisch/react";

function EmailInput({ form }) {
  const field = useField(form, { path: ["email"] });

  // 在组件逻辑中访问字段状态
  useEffect(() => {
    if (field.errors) {
      console.log("邮箱存在错误:", field.errors);
    }
  }, [field.errors]);

  return (
    <div>
      <input {...field.props} value={field.input} type="email" />
      {field.errors && <div>{field.errors[0]}</div>}
    </div>
  );
}
使用场景对比:
  • Field
    组件
    —— 在同一个组件中处理多个字段
  • useField
    Hook
    —— 单个字段且需要访问组件逻辑

Using Component Libraries

结合组件库使用

When using component libraries that don't expose their underlying native HTML elements, you cannot spread
field.props
directly. Instead, use
field.onChange
(React) or
field.onInput
(other frameworks) to update the value programmatically:
tsx
import { DatePicker } from "some-component-library";

<Field of={form} path={["date"]}>
  {(field) => (
    <DatePicker
      value={field.input}
      onChange={(newDate) => field.onChange(newDate)}
    />
  )}
</Field>;
The
field.onChange
method updates the field value and triggers validation, just like a native input would.
This is useful for:
  • Component libraries that wrap native elements without exposing them
  • Complex custom inputs like date pickers, rich text editors, or color pickers
当使用不暴露底层原生HTML元素的组件库时,无法直接扩散
field.props
。此时可使用
field.onChange
(React) 或
field.onInput
(其他框架) 以编程方式更新值:
tsx
import { DatePicker } from "some-component-library";

<Field of={form} path={["date"]}>
  {(field) => (
    <DatePicker
      value={field.input}
      onChange={(newDate) => field.onChange(newDate)}
    />
  )}
</Field>;
field.onChange
方法会更新字段值并触发验证,与原生输入的行为一致。
这适用于:
  • 组件库:封装了原生元素但未暴露的情况
  • 复杂自定义输入:如日期选择器、富文本编辑器或颜色选择器

Async Submission

异步提交

tsx
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (values) => {
  try {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(values),
    });

    if (!response.ok) {
      // Set server-side errors
      const data = await response.json();
      setErrors(form, { path: ["email"], errors: [data.error] });
    }
  } catch (error) {
    console.error("Submission failed:", error);
  }
};
tsx
const handleSubmit: SubmitHandler<typeof LoginSchema> = async (values) => {
  try {
    const response = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(values),
    });

    if (!response.ok) {
      // 设置服务端返回的错误
      const data = await response.json();
      setErrors(form, { path: ["email"], errors: [data.error] });
    }
  } catch (error) {
    console.error("提交失败:", error);
  }
};

Common Patterns

常见模式

Loading State

加载状态

tsx
<button type="submit" disabled={form.isSubmitting}>
  {form.isSubmitting ? "Submitting..." : "Submit"}
</button>
tsx
<button type="submit" disabled={form.isSubmitting}>
  {form.isSubmitting ? "提交中..." : "提交"}
</button>

Submit on Enter

按回车提交

Formisch handles this automatically via the native
<form>
element.
Formisch通过原生
<form>
元素自动处理此逻辑。

Reset After Success

提交成功后重置

tsx
const handleSubmit: SubmitHandler<typeof Schema> = async (values) => {
  await saveData(values);

  // Full reset to initial state
  reset(form);

  // Or reset but keep current input values
  reset(form, { keepInput: true });
};
tsx
const handleSubmit: SubmitHandler<typeof Schema> = async (values) => {
  await saveData(values);

  // 完全重置为初始状态
  reset(form);

  // 或重置但保留当前输入值
  reset(form, { keepInput: true });
};

Server Data Sync

服务端数据同步

When server data changes, update the baseline without losing user edits:
tsx
// After refetching data from server
reset(form, {
  initialInput: newServerData,
  keepInput: true, // Keep user's current edits
  keepTouched: true, // Keep touched state (optional)
});
当服务端数据变化时,更新基准值但不丢失用户的编辑内容:
tsx
// 从服务端重新获取数据后
reset(form, {
  initialInput: newServerData,
  keepInput: true, // 保留用户当前的编辑内容
  keepTouched: true, // 保留触碰状态(可选)
});

Conditional Fields

条件字段

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

Additional Resources

额外资源