formisch

repository·main·Indexed 21 days ago

https://github.com/open-circle/formisch

A lightweight, schema-first, and fully type-safe headless form library for React, Solid, Vue, Svelte, and more. Formisch leverages Valibot for schema-based validation and provides a framework-agnostic core engine with specific implementations for various JavaScript ecosystems, including support for React Native via @formisch/react-native.

Tokens
247.7K
Snippets
681
Records
1K
Agent score
76%

What's inside formisch

  1. Supported Frameworks for Formisch

    main

    Formisch is designed to be framework-agnostic at its core while remaining native to the specific UI framework you are using. Supported frameworks include:

    • Angular
    • Preact
    • Qwik
    • React
    • React Native
    • SolidJS
    • Svelte
    • Vue
  2. Compare React form libraries (RHF, TanStack, Formisch)

    main

    This guide compares React Hook Form (RHF), TanStack Form, and Formisch based on three key architectural pillars: TypeScript inference, validation architecture, and performance.

    Key Differences at a Glance

    FeatureReact Hook Form (RHF)TanStack FormFormisch
    Type SourceManual Generic (useForm<T>)Inferred from defaultValuesValibot Schema
    MaintenanceType + Schema + ResolverDefault Values + SchemaSchema Only
    ValidationField-centric (rules on fields)Per-validator (explicit config)Schema-centric (rules in schema)
    Re-rendersInternal object + DOM updatesAutomatic scopingAutomatic scoping (via Signals)

    TypeScript Inference Comparison

    React Hook Form (RHF)

    Types and schemas are separate. You must declare a TypeScript type and pass it as a generic to useForm. You must also provide a resolver to connect a runtime schema (Zod, Valibot, etc.) to that type. This creates a risk of

  3. Compare Formisch with React Hook Form and TanStack Form

    main

    Use this comparison to decide if Formisch is the right tool for your project based on type safety, validation, and bundle size.

    Key Differences

    FeatureFormischReact Hook FormTanStack Form
    Type sourceInferred from schemaGeneric you declareInferred from defaultValues
    Validation locationDefined in schemaPer-field or resolverPer-validator config
    Validation timingForm-wide validate / revalidateForm-wide mode optionPer-validator trigger
    Async validationBuilt-in via schemaManual loading stateBuilt-in isValidating
    Re-render scopeAutomatic per signalManual via watch / useFormStateAutomatic per subscription
    Schema librariesValibotAny via resolversStandard Schema
    Bundle size (min+gzip)From ~2.5 kB~12 kB~15 kB
    Framework supportAngular, React, React Native, Preact, Solid, Svelte, Vue, QwikReactReact, Vue, Solid, Svelte, Lit, Angular
  4. Compare Formisch with other Vue form libraries

    main

    Formisch is a headless, schema-first form library for Vue. Use this comparison to decide if Formisch fits your project requirements compared to other popular libraries like VeeValidate, FormKit, or TanStack Form.

    Key Comparison Dimensions

    FeatureFormischVeeValidateFormKitTanStack Form
    Type sourceInferred from schemaInferred from schemaDeclared manuallyInferred from defaultValues
    Validation locationDefined in schemaValidator rules or schemaPer-input prop or form schemaPer-validator config
    Validation timingForm-wide validate / revalidatePer field, configurablePer input (validation-visibility)Per-validator trigger
    Async validationBuilt-in via schemaBuilt-inBuilt-inBuilt-in isValidating
    Reactivity scopePer Vue ref subscriptionPer Vue ref subscriptionPer Vue ref subscriptionPer TanStack Store subscription
    Schema librariesValibotZod, Yup, Valibot, ArkType, etc.Built-in rules; schemas via pluginsStandard Schema
    UI approachHeadlessHeadlessComponent-driven (batteries included)Headless
    Bundle size (min+gzip)From ~2.5 kB~12 kB~25 kB+~15 kB
    Framework supportAngular, React, React Native, Preact, Solid, Svelte, Vue, QwikVue 3 (Vue 2 via legacy)Vue 3React, Vue, Solid, Svelte, Lit, Angular
  5. What is a FieldElement in React Native

    main

    In the context of Formisch for React Native, a FieldElement is a structural subset of imperative methods provided by React Native host component instances (like TextInput).

    Because React Native lacks a DOM, FieldElement acts as an abstraction that allows Formisch to interact with focusable native components via refs without requiring a direct dependency on the react-native package. This allows the field store to manage focus for any component that implements the required interface.

  6. What is a FormSchema

    main

    A FormSchema is a type definition representing the Valibot schemas allowed to serve as the root of a Formisch form.

    Because forms require an object-based structure for field mapping, a FormSchema is structurally constrained to any schema that produces an object output. This includes:

    • Object schemas
    • Combinators over objects (e.g., intersect, union, variant)
    • lazy schemas wrapping the above
    • Generic object schemas (e.g., v.GenericSchema<{ ... }>)

    Note: For schemas used for individual nested fields within a form, use the Schema type instead.

  7. What is Formisch?

    main

    Formisch is a schema-first, headless, and fully type-safe library for managing form state. It uses a single Valibot schema to drive both runtime validation and TypeScript types, eliminating the need for separate type definitions or resolvers.

    Key characteristics:

    • Headless: You maintain full control over markup and styling.
    • Type-safe: The path prop in fields is fully typed against your schema.
    • Performant: Uses fine-grained signals so only changed fields re-render.
    • Framework-agnostic core: Supports Angular, Preact, Qwik, React, React Native, SolidJS, Svelte, and Vue without an abstraction tax, as it swaps in the framework's native reactivity at build time.
  8. Key behavioral differences when migrating to Formisch

    main

    When moving from React Hook Form to Formisch, be aware of these architectural and behavioral differences:

    • Context Management: Formisch does not use a FormProvider. The form store is a plain object. You should pass it down as a prop to input components or use your own React Context if needed.
    • Field Unregistration: There is no unregister or shouldUnregister. Field existence is defined by your schema. For conditional forms, use schema features like v.optional or v.variant (Valibot) to model variants.
    • Reactivity: There is no explicit subscription API (like watch callbacks). Reading field.input or form state during render is automatically reactive.
    • Validation Triggering: The validate() method validates the entire form against the schema. Results are distributed per field, so you do not need to trigger validation on a per-field basis.
    • Error Focus: Formisch automatically focuses the first field with an error when a submission fails, following the field order defined in your schema.
    • isValid Timing: form.isValid reflects the result of the last validation run. With the default validate: 'submit', it remains true until the first submission attempt. To have it validate immediately (e.g., to disable a submit button), set validate: 'initial'.
    • Touched State: Formisch marks a field as isTouched when it receives focus, whereas React Hook Form typically marks it on blur. This means error visibility based on touched state may appear one event earlier in Formisch.
  9. Handle optional fields in schemas

    main

    Your schema should exactly reflect the data structure expected upon submission. If a field is not required, use Valibot's v.optional(...) function. Formisch validates the form values against this schema before submission, preventing submission if the data does not match the definition.

    import * as v from 'valibot';
    
    const ProfileSchema = v.object({
      name: v.pipe(v.string(), v.nonEmpty()),
      bio: v.optional(v.string()), // <- Optional field
    });
  10. How controlled fields work in Formisch

    main

    By default, Formisch fields are uncontrolled, relying on native browser behavior. However, you must use controlled fields when you need to:

    • Set initial values programmatically.
    • Manipulate field values via methods like setInput.
    • Use specific HTML attributes like value, checked, or selected to sync the UI with the form state.

    To control a field, you must explicitly bind the field's value to the input's attribute and ensure that updates are sent back to Formisch using the field's input handler.

    <Field of={loginForm} path={['firstName']}>
      {(field) => (
        <input
          {...field.props}
          type="text"
          // Pass value or empty string to ensure the input is controlled
          value={field.input ?? ''}
        />
      )}
    </Field>
  11. Manage field order and validation focus

    main

    The order of fields in your Valibot schema determines the focus behavior during validation errors. When a form is submitted with invalid values, Formisch automatically focuses the first field that failed validation.

    Best Practice: Define your fields in the schema in the same order they appear visually in your UI. This ensures that when a user submits an invalid form, the focus moves to the first invalid field at the top of the form rather than jumping to a field further down the page.

  12. The Signal interface and fine-grained reactivity

    main

    All reactive state in Formisch is managed via a Signal<T> interface. This allows for fine-grained updates: when a specific field's value changes, only the components observing that specific signal re-render, rather than the entire form.

    Every field in the store carries its own signals for:

    • errors
    • isTouched
    • isEdited
    • isDirty
    • input (for value fields)

    Structural components like objects and arrays use a children collection to manage their nested fields. Because the store hierarchy is pre-allocated based on your Valibot schema, methods can update individual signals directly without expensive object diffing or path lookups.

    interface Signal<T> {
      get value(): T;
      set value(nextValue: T): void;
    }