Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

use-standard-schema

garystorey353MIT0.2.7TypeScript support: included

A React hook for managing form state using any standard schema compliant validation library.

zod, valibot, arktype, use-standard-schema, useStandardSchema, standard schema, validation, form, react, hook, form management, form handling, form state management, react hook, form validation, react form, react validation, typescript, typescript form, typescript validation, typescript react, typescript react form

readme

useStandardSchema

A React hook for managing form state using any Standard Schema-compliant validator.

License NPM Version


Table of contents


Overview

useStandardSchema wraps a Standard Schema–compliant form definition (e.g. Zod, Valibot, ArkType, etc.) into a React hook for form handling. It streamlines validation, state, error handling, and submission—all with type safety via the Standard Schema interface.

Why useStandardSchema?

  • Works with any validator that implements the Standard Schema spec
  • Provides consistent form APIs regardless of validation library
  • Built with TypeScript support, ensuring type-safe validation and form usage
  • Integrates easily into React workflows
  • Supports nested objects with dot notation (e.g. "address.street1")

Prerequisites


Installation

npm install use-standard-schema
# or
yarn add use-standard-schema
# or
pnpm add use-standard-schema

Usage

Example using zod. This form has two fields: firstName and lastName. Both are required and must be at least two characters long.

import { defineForm, useStandardSchema, type TypeFromDefinition } from "use-standard-schema"
import * as z from "zod"

const validString = z.string().min(2, "Too short").max(100, "Too long")

// create the form definition
// nested objects are supported (e.g. address.street1)
const nameForm = defineForm({
  firstName: {
    validate: validString,  // required
    label: "First Name",     // required
    description: "Enter your given name"
  },
  lastName: {
    validate: validString,
    label: "Last Name",
    description: "Enter your surname",
    defaultValue: ""         // optional initial value
  },
})

// get the type from the form definition
type NameFormData = TypeFromDefinition<typeof nameForm>;

export function App() {
  const { getForm, getField, resetForm, getErrors } = useStandardSchema(nameForm);

  const handleSubmit = (data: NameFormData) => {
    console.log(data);
    resetForm();
  }

  const form = getForm(handleSubmit)
  const firstName = getField("firstName");
  const lastName = getField("lastName");

  const allErrors = getErrors()

  return (
    <form {...form}>

      {/* show all errors */}
      {allErrors.length > 0 && (
        <div className="all-error-messages" role="alert">
          {allErrors.map(({ key, error, label }) => (
            <p key={key}>{label} is {error}</p>
          ))}
        </div>
      )}

      <div className={`field ${firstName.error ? "has-error" : ""}`}>
        <label htmlFor={firstName.name}>{firstName.label}</label>
        <input
          name={firstName.name}
          defaultValue={firstName.defaultValue}
          aria-describedby={firstName.describedById}
          aria-errormessage={firstName.errorId}
        />
        <p id={firstName.describedById} className="description">
          {firstName.description}
        </p>
        <p id={firstName.errorId} className="error">
          {firstName.error}
        </p>
      </div>

      <div className={`field ${lastName.error ? "has-error" : ""}`}>
        <label htmlFor={lastName.name}>{lastName.label}</label>
        <input
          name={lastName.name}
          defaultValue={lastName.defaultValue}
          aria-describedby={lastName.describedById}
          aria-errormessage={lastName.errorId}
        />
        <p id={lastName.describedById} className="description">
          {lastName.description}
        </p>
        <p id={lastName.errorId} className="error">
          {lastName.error}
        </p>
      </div>

      <button type="submit">Submit</button>
    </form>
  )
}

Examples

Nested object field

Dot notation is supported automatically:

const addressForm = defineForm({
  address: {
    street1: {
      validate: z.string().min(2, "Too short"),
      label: "Street Address",
    },
  },
});

const streetField = getField("address.street1");

Valid keys

A FormDefinition's key is an intersection between a valid JSON key and an HTML name attribute.


const definition = defineForm({
    prefix: z.string(),                // valid
    "first-name": z.string(),          // valid
    "middle_name": z.string(),         // valid
    "last:name": z.string(),           // valid
    "street address": z.string()       // invalid
})

Using other validators

Switching to another validator is straightforward. Simply update validate in the form definition. Here is the example above using Valibot.

import * as v from 'valibot'

const validString = v.pipe(
    v.string(),
    v.minLength(2, "Too short"),
    v.maxLength(100, "Too long")
)

// showing the updated form definition for completeness.  
// no real changes here
const formData = defineForm({
  firstName: {
    //... the same as previous example
    validate: validString,
  },
  lastName: {
    //... the same as previous example
    validate: validString,
  }
});

In this instance, we simply update the validString validator from zod to valibot. formDefinition does not change.


Custom Components

It is recommended to use useStandardSchema with your own custom React components. This enables you to simply spread the result of the getField call directly without creating individual props. You can extend the FieldDefinitionProps interface provided.


interface FieldProps extends FieldDefinitionProps {
    // your props here
}

// or

type FieldProps = FieldDefinitionProps & {
    // your props here
}


<Field {...getField("firstName")} />
<Field {...getField("lastName")} />

API

Hook / Function Description
useStandardSchema(formDefinition) Initialize form state and validation with a form definition
getForm(onSubmit) Returns event handlers for the form; submit handler only fires with valid data
getField(name) Returns metadata for a given field (label, defaultValue, error, touched, dirty, ARIA ids)
resetForm() Resets all form state to initial defaults
touched Read-only frozen object of touched fields
dirty Read-only frozen object of dirty fields
toFormData(data) Helper to convert values to FormData
getErrors(name?) Returns an array of { name, error, label } for field or form
validate(name?) Validates either the entire form or a single field
__dangerouslySetField(name, value) Sets a field’s value directly and validates it

Best Practices

  • Type Safety: Use TypeFromDefinition<typeof form> for your submit handler if you need type safety. This ensures your form data matches the form definition.
  • Error Display: Use getErrors() for global errors and field.error for field-level errors.
  • Performance: Handlers and derived values (getForm, getField, getErrors) are memoized internally. You don’t need extra useMemo unless you’re doing heavy custom work.
  • Reset Strategy: Call resetForm() after successful submission to clear touched/dirty/errors and restore defaults.
  • Nested Fields: Use dot notation for nested keys (e.g. "address.street1"). TypeScript support ensures autocomplete for these paths.
  • Accessibility: Always wire describedById and errorId into your markup to keep your forms screen-reader friendly.
    • getField provides describedById and errorId for use with aria-describedby and/or aria-errormessage.
    • Ensures that developers can add proper screen reader support for error messages.
    • Group-level errors can be presented with role="alert".

Feedback & Support

If you encounter issues or have feature requests, open an issue on GitHub.


ChangeLog

  • v0.2.7 - Improve error handling
    • Update the return of getErrors to be {name, label, error} for consistency.
    • getErrors will name accept an optional name prop and return only that error.
    • Add FieldDefinitionProps interface for easy extension for custom components.
  • v0.2.6 - Better error handling
    • Add label to type ErrorEntry. This allows users to use the label in error messages.
  • v0.2.5 - Add tests.
    • Add vitest and testing-library.
    • Add tests for all existing functionality.
    • Created a stricter FormDefinition type.
      • Keys must be an intersection of a valid json key and an html name attribute.
  • v0.2.4 - Improve validation.
    • remove "schema" from function names internally and externally.
    • Validation is handled consistently internally.
    • Update getErrors to return ordered { key, error }[].
    • fix issue with resetForm not clearing form
  • v0.2.3 - Fix recursion error in isFormDefinition that caused an infinite loop.
  • v0.2.2 - Fix recursion error in flattenSchema.
  • v0.2.1 - Rename defineSchema to defineForm. Rename schema to validate.
  • v0.2.0 - Add nested object support.
  • v0.1.0 - Initial release.

License

MIT License