use-standard-schema
v0.4.3
Published
A React hook for managing form state using any standard schema compliant validation library.
Maintainers
Readme
useStandardSchema
A React hook for managing form state using any Standard Schema-compliant validator.
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 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
- React v18+
- TypeScript (optional, but recommended)
- A validator that implements the Standard Schema v1 interface
Installation
npm install use-standard-schema
# or
yarn add use-standard-schema
# or
pnpm add use-standard-schemaUsage
Define your form once with defineForm, then consume it inside a component with useStandardSchema.
import { defineForm, useStandardSchema, type TypeFromDefinition } from "use-standard-schema"
import * as z from "zod"
const subscriptionForm = defineForm({
email: {
label: "Email",
validate: z.email("Enter a valid email address"),
defaultValue: "", // optional
description: "We'll send occasional updates.", // optional
},
})
function onSubmitHandler (values: TypeFromDefinition<typeof subscriptionForm>) {
console.log("Submitted:", values)
}
export function SubscriptionPage() {
const { getForm, getField } = useStandardSchema(subscriptionForm)
const formHandlers = getForm(onSubmitHandler)
const email = getField("email")
return (
<form {...formHandlers}>
<label htmlFor={email.name}>{email.label}</label>
<input
id={email.name}
name={email.name}
defaultValue={email.defaultValue}
// value={defaultValue}
aria-describedby={email.describedById}
aria-errormessage={email.errorId}
/>
<p id={email.describedById}>{email.description}</p>
<p id={email.errorId} role="alert">{email.error}</p>
<button type="submit">Subscribe</button>
</form>
)
}getForm(onSubmit): Returns event handlers for the<form>.onSubmitonly runs when valid.getField(name): Returns the given field's metadata.
Examples
Additional examples are available.
- CodeSandbox Demo - Try the hook in a live React playground.
- Dependent Fields example - An example that keeps two related fields in sync using
setFieldandsetError. - Custom Component example - Share reusable inputs via
FieldData. - Valibot example - Build a simple login form powered by Valibot validators.
- Shadcn Field example - Wire
useStandardSchemametadata into the shadcn/uiFieldprimitives.
Nested object fields
Nested objects are supported.
import { defineForm } from "use-standard-schema"
import * as z from "zod"
const addressForm = defineForm({
address: {
street1: { label: "Street", validate: z.string().min(2) },
},
})
Error handling
useStandardSchema returns the getErrors method that returns all of the current validations errors. This can be useful for giving all form error messages in one location. NOTE: This is in addition to the getField method which returns the errors for a given field.
import type { ErrorEntry } from "use-standard-schema"
const { getErrors } = useStandardSchema(loginForm)
const errors = getErrors()
{errors.length > 0 && (
<div role="alert">
{errors.map(({ name, label, error }: ErrorEntry) => (
<p key={name}>{label}: {error}</p>
))}
</div>
)}Touched and dirty helpers
Use the isTouched and isDirty helper methods to check whether or not the form, or a given field, has been modified or focused by the user.
const { isTouched, isDirty } = useStandardSchema(addressForm)
const isStreetTouched = isTouched("address.street1")
const isStreetDirty = isDirty("address.street1")
const isFormTouched = isTouched()
const isFormDirty = isDirty()Valid keys
A FormDefinition's key is an intersection between a valid JSON key and an HTML name attribute.
const formDefinition = 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
})
API
useStandardSchema returns a helpers for wiring form elements, reading state, and issuing manual updates.
useStandardSchema(formDefinition)
Passing a form definition using defineForm and pass the definition to the hook. The return value exposes the rest of the helpers documented below.
const { getForm, getField, getErrors, setField, setError, resetForm, isTouched, isDirty, watchValues } =
useStandardSchema(myFormDefinition)getForm(onSubmit)
Returns the event handlers for the <form>. It validates all fields and only invokes your handler when everything passes.
const form = getForm((values) => console.log(values))
return <form {...form}>...</form>getField(name)
Returns metadata for a specific field for wiring inputs, labels, and helper text.
const email = getField("email")
<input
name={email.name}
defaultValue={email.defaultValue}
aria-describedby={email.describedById}
aria-errormessage={email.errorId}
aria-invalid={!!email.error}
/>
<span id={email.describedById}>Enter your email address</span>
<span id={email.errorId}>{email.error}</span>getErrors(name?)
Returns structured error data of type ErrorEntry for the whole form or for one specific field - perfect for summary banners or toast notifications.
const allErrors = getErrors()
const emailErrors = getErrors("email")resetForm()
Clears errors, touched/dirty flags, and restores the original defaults. Note: The hook calls this automatically after a successful submit.
<button type="reset" onClick={resetForm}>Reset<button>isTouched(name?) and isDirty(name?)
Report whether a field - or any field when called without arguments - has been interacted with or changed.
const hasEditedAnything = isDirty()
const isEmailTouched = isTouched("email")watchValues(targets?, callback)
Subscribe to canonical form values without forcing extra React renders. The callback executes whenever any watched key changes and receives an object scoped to those fields.
Parameters
targets(optional): a single field name or array of field names. Omit to observe every value in the form.callback(values): invoked with the latest values for the watched fields.
Returns
unsubscribe(): stop listening insideuseEffectcleanups or teardown handlers.
const postToPreview = ({ plan, seats }) => {
previewChannel.postMessage({
quote: calculateQuote(plan, Number(seats))
})
}
useEffect(() => {
const unsubscribe = watchValues(["plan", "seats"], postToPreview)
return unsubscribe
}, [watchValues])toFormData(data)
Helper that converts a values object into a browser FormData instance for interoperability with fetch/XHR uploads.
const formData = toFormData(values)setField(name, value)
Updates a field's value (for dependent fields, custom widgets, or multi-step wizards) and re-validates it. NOTE: You do not need to call this manually in most situations. It will occur automatically.
setField("address.postalCode", nextPostalCode)setError(name, error)
Sets a manual error message for any field (for dependent fields, custom widgets, or multi-step wizards). Pass null or undefined to clear it. NOTE: You do not need to call this manually in most situations. It will occur automatically.
setError("email", new Error("Email already registered"))Feedback & Support
If you encounter issues or have feature requests, open an issue on GitHub.
Changelog
- v0.4.3
- Fixed documentation issues.
- Fixed missing
ErrorEntryexport.
- v0.4.2
- Added
watchValuesfor monitoring value changes without rerender. - Fixed issue with
ErrorInfonot being exported. - Field updates are safer, validation errors fall back to helpful defaults, and async checks no longer overwrite newer input.
- Added a shadcn/ui Field example
- Added additional tests to keep real-world flows covered.
- Added
- v0.4.1 - Minor code fixes and documentation updates
- v0.4.0 - Improved form state synchronization, renamed the
FieldDefinitionPropstype toFieldData, and ensured programmatic updates stay validated while tracking touched/dirty status. - View the full changelog for earlier releases.
