🪱 Zorm - Type-safe
for React using Zod

Related tags

React react-zorm
Overview

React Zorm

Type-safe <form> for React using Zod!

Features / opinions

  • 💎 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
    • Via FormData (Remix! 💜 ) and JSON
  • 👍 Tiny: Less than 3kb (minified & gzipped, not including Zod)
  • 🛑 No controlled inputs required
    • 🚀 As performant as React form libraries can get!
    • You can still use your own controlled inputs if needed
  • 🛑 No components, just a React hook
    • 🧳 Bring your own UI!
  • 🛑 No internal form state
    • The form is validated directly from the <form> DOM element

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

Install

npm install react-zorm

Example

Also on Codesandbox!

import { z } from "zod";
import { useZorm } from "react-zorm";

const FormSchema = z.object({
    name: z.string().min(1),
    age: z
        .string()
        .regex(/^[0-9]+$/)
        .transform(Number),
});

function Signup() {
    const zo = useZorm("signup", FormSchema, {
        onValidSubmit(e) {
            e.preventDefault();
            alert("Form ok!\n" + JSON.stringify(e.data, null, 2));
        },
    });
    const disabled = zo.validation?.success === false;

    return (
        <form ref={zo.ref}>
            Name:
            <input
                type="text"
                name={zo.fields.name()}
                className={zo.errors.name("errored")}
            />
            {zo.errors.name((e) => (
                <ErrorMessage message={e.message} />
            ))}
            Age
            <input
                type="text"
                name={zo.fields.age()}
                className={zo.errors.age("errored")}
            />
            {zo.errors.age((e) => (
                <ErrorMessage message="Age must a number" />
            ))}
            <button disabled={disabled} type="submit">
                Signup!
            </button>
            <pre>Validation status: {JSON.stringify(zo.validation, null, 2)}</pre>
        </form>
    );
}

Also checkout this classic TODOs example demonstrating almost every feature in the library and if you are in to Remix checkout this server-side validation 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.:

<input type="text" name={zo.fields.user.email()} />;
<input type="password" name={zo.fields.user.password()} />;

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 (
        <>
            <input type="text" name={zo.fields.users(index).email()} />
            <input type="password" name={zo.fields.users(index).password()} />
        </>
    );
});

And all this is type checked 👌

See the TODOs example 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:

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

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).
  • <T>(render: (issue: 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. Useful for rendering error message components
  • (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({ form: zo.ref, name: zo.fields.input() });
    return <form ref={zo.ref}>...</form>;
}

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 <Value> wrapper which allows access to the input value via render prop:

import { Value } from "react-zorm";

function Form() {
    const zo = useZorm("form", FormSchema);
    return (
        <form ref={zo.ref}>
            <input type="text" name={zo.fields.input()} />
            <Value form={zo.ref} name={zo.fields.input()}>
                {(value) => <span>Input value: {value}</span>}
            </Value>
        </form>
    );
}

This way only the inner <span> 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 (
        <form
            ref={zo.ref}
            onSubmit={(e) => {
                const validation = zo.validate();

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

How to handle 3rdparty components?

That do not create <input> elements?

Since Zorm just works with the native <form> you must sync their state to <input type="hidden"> elements in order for them to become actually part of the form.

Here's a Codesandbox example with react-select.

How to validate dependent fields like password confirm?

See https://twitter.com/esamatti/status/1488553690613039108

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;

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 your forms.

param schema: ZodObject

Zod schema to parse the form with.

param options?: UseZormOptions

  • onValidSubmit(event: ValidSubmitEvent): any: Called when the form is submitted with valid data
    • ValidSubmitEvent#data: The Zod parsed form data
    • ValidSubmitEvent#target: The form HTML Element
    • ValidSubmitEvent#preventDefault(): Prevent the default form submission
  • setupListeners: boolean: Do not setup any listeners. Ie. onValidSubmit won't be called nor the submission is automatically prevented. This gives total control when to validate the form. Set your own onSubmit on the form etc. Defaults to true.
  • customIssues: ZodIssue[]: Any additional ZodIssue to be rendered within the error chain. This is commonly used to handle server-side field validation

return Zorm

  • ref: HTMLFormElement ref for the <form> element
  • validation: SafeParseReturnType | null: The current Zod validation status returned by safeParse()
  • validate(): SafeParseReturnType: Manually invoke validation
  • fields: FieldChain: The fields chain
  • errors: ErroChain: The error chain

Zorm Type

The type of the object returned by useZorm(). This type object can be used to type component props if you want to split the form to multiple components and pass the zorm object around.

import type { Zorm } from "react-zorm";

function MyForm() {
    const zo = useZorm("signup", FormSchema);

    return (
        // ...
        <SubComponent zorm={zo} />
        //..
    );
}

function SubComponent(props: { zorm: Zorm<typeof FormSchema> }) {
    // ...
}

useValue(subscription: ValueSubscription): string

Get live raw value from the input.

ValueSubscription

  • form: RefObject<HTMLFormElement>: The form ref from zo.ref
  • initialValue: string: Initial value on the first and ssr render
  • transform(value: string): any: Transform the value before setting it to the internal state. The type can be also changed.

Value: React.Component

Render prop version of the useValue() hook. The props are ValueSubscription. The render prop child is (value: string) => ReactNode.

<Value form={zo.ref} name={zo.fields.input()}>
    {(value) => <>value</>}
</Value>

parseForm(form: HTMLFormElement | FormData, schema: ZodObject): Type<ZodObject>

Parse HTMLFormElement or FormData with the given Zod schema.

safeParseForm(form, schema): SafeParseReturnType

Like parseForm() but uses the safeParse() method from Zod.

Comments
  • Controlled mode 👀

    Controlled mode 👀

    I love the idea of this library. I always thought Zod and Forms feels like a match made in heaven and this library is a great demonstration of an ergonomic API that leverages Zod.

    However, whilst uncontrolled mode leads to good perf (and is partly why react-hooks-form got popular), you lose a lot of stuff (I think? I'm wanting to be wrong!). Things like:

    • Having nested forms which when saved, update the values of the parent form.
    • Having form field values which update when some other action/form field changes.
    • Wizards, where form state is built up over time, and only a subset of controls are rendered at a given time.

    For really complex form flows, its difficult without being in controlled mode.

    But what about perf? Well...React 18 to the rescue (?). Ive started a conversation here on Formik which outlines the idea of utilizing useTransition to get the best of both worlds: https://github.com/jaredpalmer/formik/issues/3532

    opened by adam-thomas-privitar 7
  • Can I use with Radix UI Select?

    Can I use with Radix UI Select?

    Trying to stitch together RadixUI's Select and my react-zorm form but can't get the values to sync.

    This is my code:

    export const MyComp = () => {
    
      const zo = useZorm("update-formatting", UpdateFormSchema, {
        onValidSubmit(e) {
          e.preventDefault();
          console.log("FormData:", e.data);
        },
      });
    
      return (
        <div>
          {/* Form */}
          <div>
            <form ref={zo.ref}>
              {/* Language Selector */}
              <div>
                <label
                  htmlFor={zo.fields.language("id")}
                >
                  Language
                </label>
                <Select.Root
                  defaultValue={language}
                  name={zo.fields.language("name")}
                  onValueChange={(value) => {
                    console.log("Lang onChange", value);
                  }}
                >
                  <Select.Trigger>
                    <Select.Value aria-label={language}>{language}</Select.Value>
                    <Select.Icon />
                  </Select.Trigger>
    
                  <Select.Portal>
                    <Select.Content>
                      <Select.Viewport>
                        {langs.map((item) => (
                          <Select.Item
                            key={item.key}
                            value={item.key}
                          >
                            <Select.ItemText>
                              <item.icon />
                              <p>{item.key}</p>
                            </Select.ItemText>
                            <Select.ItemIndicator>
                              <HiCheckCircle />
                            </Select.ItemIndicator>
                          </Select.Item>
                        ))}
                      </Select.Viewport>
                    </Select.Content>
                  </Select.Portal>
                </Select.Root>
              </div>
            </form>
         </div>
      );
    };
    

    Which renders the following DOM:

    image

    But the onChange Radix gives doesn't change the selected item in the select:

    https://user-images.githubusercontent.com/51714798/205437256-a3f3ef7c-aef7-4bd6-9e6c-d40cc8d0c90e.mp4

    Any idea what I'm doing wrong?

    opened by juliusmarminge 6
  • Add comparison to other liberaries such as react-hook-form

    Add comparison to other liberaries such as react-hook-form

    Hi,

    Would be cool to see a small comparison to the most popular form libraries out there. Seems that react-zorm shares quite a bunch of similarities with react-hook-form, but it is not fully clear what are the differences for example.

    opened by villesau 3
  • Does not throw on parse errors

    Does not throw on parse errors

    Hey there! Loving this library so thank you for the hard work and for open sourcing it!

    Today I was thinking that for consistency sake it'd be nice if parseForm used zod's safeParse instead of parse therefore returning the error(s) instead of throwing.

    This would play better with createCustomIssues in that one could combine those with parseForm's errors and handle errors in a consistent way.

    Please let me know if I am missing something because the library is still new to me ✌️

    opened by giuseppeg 2
  • Unusable with `react-modal` (no `form` dom element → no `submit` event bound).

    Unusable with `react-modal` (no `form` dom element → no `submit` event bound).

    react-modal is usually always mounted, but it's children are not in the DOM until after it's isOpen prop turns true.

    My guess is this should be fixable by changing useEffect to useLayoutEffect in useZorm here: https://github.com/esamattis/react-zorm/blob/46c32bc2314e57bd0032d3e16053dee4149a9717/packages/react-zorm/src/use-zorm.tsx#L64 and recommending that users pass setupListeners: isOpen to useZorm.

    Also this workaround works for me:

    // useZorm.ts
    import { useLayoutEffect, useState } from "react";
    import { Zorm, useZorm as useZormBase } from "react-zorm";
    import { UseZormOptions } from "react-zorm/dist/use-zorm";
    import { ZodType } from "zod";
    
    export function useZorm<Schema extends ZodType<any>>(
    	formName: string,
    	schema: Schema,
    	options?: UseZormOptions<ReturnType<Schema["parse"]>>,
    ): Zorm<Schema> {
    	const [ setupListeners, setSetupListeners ] = useState(options?.setupListeners);
    
    	useLayoutEffect(() => {
    		setSetupListeners(options?.setupListeners);
    	}, [
    		options?.setupListeners,
    	]);
    
    	return useZormBase(formName, schema, {
    		...options,
    		setupListeners,
    	});
    }
    
    // MyModal.tsx
    import { z } from 'zod';
    import Modal from "react-modal";
    import { useZorm } from './useZorm';
    
    const formSchema = z.object({
    	email: z.string().email(),
    });
    
    export interface MyModalProps {
    	isOpen: boolean;
    	onRequestClose: () => void;
    }
    
    export function MyModal({
    	isOpen,
    	onRequestClose,
    }: MyModalProps) {
    	const zo = useZorm('myModalForm', formSchema, {
    		setupListeners: isOpen,
    		onValidSubmit(event) {
    			// never called, instead native form submit happens (page reload)
    			debugger;
    			event.preventDefault();
    		},
    	});
    
    	return (
    		<Modal
    			isOpen={isOpen}
    			onRequestClose={onRequestClose}
    		>
    			<form
    				ref={zo.ref}
    			>
    				<input
    					name={zo.fields.email()}
    					type="email"
    				/>
    
    				<button
    					type="submit"
    				>
    					submit
    				</button>
    			</form>
    		</Modal>
    	);
    }
    
    opened by futpib 2
  • Incorrect types - schema with z.date() does not result in FieldGetter

    Incorrect types - schema with z.date() does not result in FieldGetter

    Hi,

    I've got a field with a date picker that I'm casting from a string to a date with z.preprocess, but TS incorrectly warns me that I can't call the field name getter

    <input type="date" name={form.fields.date()} />
                                         ^^^^^^
    This expression is not callable.
      Type 'FieldChain<{ toString: {} [..OMITTED..] }>' has no call signatures.
    

    form.errors.date seems to work fine however

    Codesandbox repro

    Happy to attempt a PR patching it if you'd like and can point me in the right direction

    opened by mcky 2
  • Add support for server side errors

    Add support for server side errors

    React Zorm could have another source (other than zod) for errors which could be used for server-side errors which would be read by the error chain.

    Something like:

    const zo = useZorm("contact", FormSchema, {
        serverErrors: [
            {
                path: ["user", "userName"],
                message: "Username is already in use",
            }
        ],
    });
    

    where you would of course define the serverErrors array on the server

    Originally posted by @esamattis in https://github.com/esamattis/react-zorm/issues/10#issuecomment-1091729960

    opened by esamattis 1
  • Bug: ReferenceError: document is not defined

    Bug: ReferenceError: document is not defined

    I'm trying react-zorm with Remix and run into this issue.

    https://github.com/esamattis/react-zorm/blob/6deb4270501766b12f968bf147114c858bd29a84/packages/react-zorm/src/use-zorm.tsx#L45

    I think in here it should be

        if (typeof document !== "undefined") {
            // ...
        }
    
    opened by alexluong 1
  • How to handle validation errors thrown from Remix actions?

    How to handle validation errors thrown from Remix actions?

    Hi, really like the idea of react-zorm and am curious if you have any thoughts on this topic?

    Let's say for some reason the form go through even if there's some validation error, how should we handle the thrown response from Remix action?

    opened by alexluong 1
  • v0.2.0

    v0.2.0

    • Remove .props() now just .ref and setup listeners on that directly
    • Pass setupListeners: false to disable the default validating behaviour
    • Remove old prototype API
    • More tests
    opened by esamattis 0
  • Thoughts on HTML attributes utility

    Thoughts on HTML attributes utility

    I would love it if this library had some utility that allows me to pass a schema and get back any possible relevant HTML validation attributes.

    For example, taking the example on the README:

                  Name:
                  <input
    -                 type="text"
    -                 name={zo.fields.name()}
    -                 className={zo.errors.name("errored")}
    +                 {...zo.fields.name.getProps({
    +                   className: zo.errors.name("errored")
    +                 })}
                  />
    

    Just an idea. That way we can get some sweet progressive enhancement going on.

    enhancement 
    opened by kentcdodds 2
Releases(react-zorm/v0.6.1)
Owner
Esa-Matti Suuronen
I write code and jump from airplanes
Esa-Matti Suuronen
🏁 High performance subscription-based form state management for React

You build great forms, but do you know HOW users use your forms? Find out with Form Nerd! Professional analytics from the creator of React Final Form.

Final Form 7.2k Jan 7, 2023
ESLint plugin for react-hook-form

eslint-plugin-react-hook-form react-hook-form is an awsome library which provide a neat solution for building forms. However, there are many rules for

Chuan-Tse Kao 37 Nov 22, 2022
An easy-to-use super customisable form validation library for React.

An easy-to-use super customisable form validation library for React. This library handles all of your form states using built in useReducer hook of react.

Himanshu Bhardwaz 2 Jun 30, 2022
Form handler in React-MobX

MobX Light Form ✨ MobX Form State Management with automatic validation Seperate subjects which manage form data to prevent duplication of data and ens

정윤재 3 Nov 30, 2022
An example of a schema-based form system for React.

React Advanced Form An example of a schema-based form system in React. Define your schema, and pass it into the form. Supports basic conditional schem

Tania Rascia 111 Dec 31, 2022
A simple tool that tells you what food is safe for your dog.

Can My Dog Eat A simple tool that tells you what food is safe for your dog. View website Features Can My Dog Eat is a simple website where you can loo

Isabelle Viktoria Maciohsek 25 Dec 11, 2022
Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.

Recoil · Recoil is an experimental set of utilities for state management with React. Please see the website: https://recoiljs.org Installation The Rec

Facebook Experimental 18.2k Jan 8, 2023
Fill the boring catsalud covid vaccine form with a console command

vacunacovid-catsalud-autofullfill form Fill the boring catsalud covid vaccine form with a console command Manual use, pasting in the script in the con

null 18 Jul 27, 2021
Phonemask - Library for processing the phone input field in the web form. Only native javascript is used

phonemask Library for processing the phone input field in the web form. Only native javascript is used Usage: Adding a library to HTML <script type="a

Neovav 2 Sep 20, 2022
A Drag & Drop Form Builder base on Bootstrap v4.x

bsFormBuilder 一个基于 Bootstrap (v4.x) + JQuery 的、拖拽的表单构建工具。 特点 1、基于 Bootstrap (v4.x) + JQuery,简单易用 2、拖动的 html 组件,支持通过 Json 自定义扩展 3、组件的属性面板,支持通过 Json 自定义

Michael Yang 10 Aug 25, 2022
Generate font size variables for a fluid type scale with CSS clamp.

Fluid Type Scale Calculator Generate font size variables for a fluid type scale with CSS clamp. Overview Customize everything, grab the output CSS, an

Aleksandr Hovhannisyan 136 Dec 31, 2022
React features to enhance using Rollbar.js in React Applications

Rollbar React SDK React features to enhance using Rollbar.js in React Applications. This SDK provides a wrapper around the base Rollbar.js SDK in orde

Rollbar 39 Jan 3, 2023
Soft UI Dashboard React - Free Dashboard using React and Material UI

Soft UI Dashboard React Start your Development with an Innovative Admin Template for Material-UI and React. If you like the look & feel of the hottest

Creative Tim 182 Dec 28, 2022
React-app - Building volume rendering web app with VTK.js,react & HTML Using datasets provided in vtk examples (head for surface rendering and chest for ray casting)

SBE306 Assignment 4 (VTK) data : Directory containing Head and Ankle datasets Description A 3D medical viewer built with vtk-js Team Team Name : team-

Mustafa Megahed  2 Jul 19, 2022
a more intuitive way of defining private, public and common routes for react applications using react-router-dom v6

auth-react-router is a wrapper over react-router-dom v6 that provides a simple API for configuring public, private and common routes (React suspense r

Pasecinic Nichita 12 Dec 3, 2022
A react component available on npm to easily link to your project on github and is made using React, TypeScript and styled-components.

fork-me-corner fork-me-corner is a react component available on npm to easily link to your project on github and is made using React, TypeScript and s

Victor Dantas 9 Jun 30, 2022
React Starter Kit — isomorphic web app boilerplate (Node.js, Express, GraphQL, React.js, Babel, PostCSS, Webpack, Browsersync)

React Starter Kit — "isomorphic" web app boilerplate React Starter Kit is an opinionated boilerplate for web development built on top of Node.js, Expr

Kriasoft 21.7k Dec 30, 2022
📋 React Hooks for forms validation (Web + React Native)

English | 繁中 | 简中 | 日本語 | 한국어 | Français | Italiano | Português | Español | Русский | Deutsch | Türkçe Features Built with performance and DX in mind

React Hook Form 32.4k Dec 29, 2022
:black_medium_small_square:React Move | Beautiful, data-driven animations for React

React-Move Beautiful, data-driven animations for React. Just 3.5kb (gzipped)! Documentation and Examples Features Animate HTML, SVG & React-Native Fin

Steve Hall 6.5k Jan 1, 2023