Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
R

react-zorm

> 编程语言
Open source

Zorm - Type-safe <form> for React using Zod

734 stars0 likes0 views
WebsiteGitHub

About

Zorm - Type-safe <form> for React using Zod

React Zorm

Type-safe `` for React using Zod!

Features / opinions

  • NEW experimental automatic progressive HTML attributes helper
-   Docs and [feedback here](https://github.com/esamattis/react-zorm/discussions/48).
  • Type-safe
-   Get form data as a typed object
-   Typo-safe `name` and `id` attribute generation
  • Simple nested object and array fields
-   And still type-safe!
  • ✅ Validation on the client and the server
    • With FormData or JSON
    • Eg. works with any JavaScript backend
    • Remix, Next.js, Express, Node.js, CF Workers, Deno etc.
  • Tiny: Less than 3kb (minified & gzipped)
-    Tree shakes to be even smaller!
-    No dependencies, only peer deps for React and Zod
  • No controlled inputs or context providers required
-   ☝️ The form is validated directly from the `` DOM element
-    As performant as React form libraries can get!

If you enjoy this lib a Twitter shout-out @esamatti is always welcome!

You can also checkout my talk at React Finland 2022. Slides.

Install

npm install react-zorm

Example

Also on Codesandbox!

…

Also checkout [this classic TODOs example][todos] demonstrating almost every feature in the library and if you are in to Remix checkout [this server-side validation example][remix-example].

Nested data

Objects

Create a Zod type with a nested object

const FormSchema = z.object({
    user: z.object({
        email: z.string().min(1),
        password: z.string().min(8),
    }),
});

and just create the input names with .user.:

;
;

Arrays

Array of user objects for example:

const FormSchema = z.object({
    users: z.array(
        z.object({
            email: z.string().min(1),
            password: z.string().min(8),
        }),
    ),
});

and put the array index to users(index):

users.map((user, index) => {
    return (
        <>
            
            
        
    );
});

And all this is type checked

See the [TODOs example][todos] for more details

Server-side validation

This is Remix but React Zorm does not actually use any Remix APIs so this method can be adapted for any JavaScript based server.

import { parseForm } from "react-zorm";

export let action: ActionFunction = async ({ request }) => {
    const form = await request.formData();
    // Get parsed and typed form object. This throws on validation errors.
    const data = parseForm(FormSchema, form);
};

Server-side field errors

The useZorm() hook can take in any additional ZodIssues via the customIssues option:

const zo = useZorm("signup", FormSchema, {
    customIssues: [
        {
            code: "custom",
            path: ["username"],
            message: "The username is already in use",
        },
    ],
});

These issues can be generated anywhere. Most commonly on the server. The error chain will render these issues on the matching paths just like the errors coming from the schema.

To make their generation type-safe react-zorm exports createCustomIssues() chain to make it easy:

const issues = createCustomIssues(FormSchema);

issues.username("Username already in use");

const zo = useZorm("signup", FormSchema, {
    customIssues: issues.toArray(),
});

This code is very contrived but take a look at these examples:

  • [Rendering field errors from the Remix Action Functions][remix-example]
  • Async validation via React Query

The Chains

The chains are a way to access the form validation state in a type safe way. The invocation via () returns the chain value. On the fields chain the value is the name input attribute and the errors chain it is the possible ZodIssue object for the field.

There few other option for invoking the chain:

fields invocation

Return values for different invocation types

  • ("name"): string - The name attribute value
  • ("id"): string - Unique id attribute value to be used with labels and aria-describedby
  • (): string - The default, same as "name"
  • (index: number): FieldChain - Special case for setting array indices
  • (fn: RenderFunction): any - Calls the function with {name: string, id: string, type: ZodType, issues: ZodIssue} and renders the return value.
    • Can be used to create resuable fields. Codesandbox example.

errors invocation

  • (): ZodIssue | undefined - Possible ZodIssue object
  • (value: T): T | undefined - Return the passed value on error. Useful for setting class names for example
  • (value: typeof Boolean): boolean - Return true when there's an error and false when it is ok. Example .field(Boolean).
  • (render: (issue: ZodIssue, ...otherIssues: ZodIssue[]) => T): T | undefined - Invoke the passed function with the ZodIssue and return its return value. When there's no error a undefined is returned and the function will not be invoked. Useful for rendering error message components. One field can have multiple issues so to render them all you can use the spread operator ...issues.
  • (index: number): ErrorChain - Special case for accessing array elements

Using input values during rendering

The first tool you should reach is React. Just make the input controlled with useState(). This works just fine with checkboxes, radio buttons and even with text inputs when the form is small. React Zorm is not really interested how the inputs get on the form. It just reads the value attributes using the platform form APIs (FormData).

But if you have a larger form where you need to read the input value and you find it too heavy to read it with just useState() you can use useValue() from Zorm.

import { useValue } from "react-zorm";

function Form() {
    const zo = useZorm("form", FormSchema);
    const value = useValue({ zorm: zo, name: zo.fields.input() });
    return ...;
}

useValue() works by subscribing to the input DOM events and syncing the value to a local state. But this does not fix the performance issue yet. You need to move the useValue() call to a subcomponent to avoid rendering the whole form on every input change. See the Zorm type docs on how to do this.

Alternatively you can use the `` wrapper which allows access to the input value via render prop:

import { Value } from "react-zorm";

function Form() {
    const zo = useZorm("form", FormSchema);
    return (
        
            
            
                {(value) => Input value: {value}}
            
        
    );
}

This way only the inner `` element renders on the input changes.

Here's a codesandox demonstrating these and vizualizing the renders.

FAQ

When Zorm validates?

When the form submits and on input blurs after the first submit attempt.

If you want total control over this, pass in setupListeners: false and call validate() manually when you need. Note that now you need to manually prevent submitting when the form is invalid.

function Signup() {
    const zo = useZorm("signup", FormSchema, { setupListeners: false });

    return (
         {
                const validation = zo.validate();

                if (!validation.success) {
                    e.preventDefault();
                }
            }}
        >
            ...
        
    );
}

How to handle 3rdparty components?

That do not create `` elements?

Since Zorm just works with the native you must sync their state to elements in order for them to become actually part of the form.

Here's a Codesandbox example with react-select.

Another more modern option is to use the formdata event. Codesandbox example

How to validate dependent fields like password confirm?

See

How to translate form error messages to other languages?

Use the ZodIssue's .code properties to render corresponding error messages based on the current language instead of just rendering the .message.

See this Codesandbox example:

How to use checkboxes?

Checkboxes can result to simple booleans or arrays of selected values. These custom Zod types can help with them. See this usage example.

const booleanCheckbox = () =>
    z
        .string()
        // Unchecked checkbox is just missing so it must be optional
        .optional()
        // Transform the value to boolean
        .transform(Boolean);

const arrayCheckbox = () =>
    z
        .array(z.string().nullish())
        .nullish()
        // Remove all nulls to ensure string[]
        .transform((a) => (a ?? []).flatMap((item) => (item ? item : [])));

How to do server-side validation without Remix?

If your server does not support parsing form data to the standard FormData you can post the form as JSON and just use .parse() from the Zod schema. See the next section for JSON posting.

How to submit the form as JSON?

Prevent the default submission in onValidSubmit() and use fetch():

const zo = useZorm("todos", FormSchema, {
    onValidSubmit: async (event) => {
        event.preventDefault();
        await fetch("/api/form-handler", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
            },
            body: JSON.stringify(event.data),
        });
    },
});

If you need loading states React Query mutations can be cool:

import { useMutation } from "react-query";

// ...

const formPost = useMutation((data) => {
    return fetch("/api/form-handler", {
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(data),
    });
});

const zo = useZorm("todos", FormSchema, {
    onValidSubmit: async (event) => {
        event.preventDefault();
        formPost.mutate(event.data);
    },
});

return formPost.isLoading ? "Sending..." : null;

How to upload and validate files?

Use z.instanceof(File) for the file input type. See this Codesandox for an example.

Native forms support files as is but if you need to POST as JSON you can turn the file to a base64 for example. See FileReader.readAsDataURL(). Or just post the file separately.

API

Tools available for importing from "react-zorm"

useZorm(formName: string, schema: ZodObject, options?: UseZormOptions): Zorm

Create a form Validator

param formName: string

The form name. This used for the input id generation so it should be unique string within

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

TypeScript

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言