{"id":13767670,"url":"https://github.com/esamattis/react-zorm","last_synced_at":"2025-05-15T18:09:32.615Z","repository":{"id":40590049,"uuid":"453757724","full_name":"esamattis/react-zorm","owner":"esamattis","description":"🪱 Zorm - Type-safe \u003cform\u003e for React using Zod","archived":false,"fork":false,"pushed_at":"2024-03-16T04:33:10.000Z","size":854,"stargazers_count":734,"open_issues_count":9,"forks_count":10,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-31T22:17:56.475Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/react-zorm","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/esamattis.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-30T17:58:46.000Z","updated_at":"2025-03-31T15:23:46.000Z","dependencies_parsed_at":"2024-01-25T23:08:33.912Z","dependency_job_id":"407d4b6d-0126-40c1-9d5c-84f6b5c7ffd7","html_url":"https://github.com/esamattis/react-zorm","commit_stats":{"total_commits":298,"total_committers":6,"mean_commits":"49.666666666666664","dds":0.2348993288590604,"last_synced_commit":"0b4355a37415b974cab9e1e276958aaaf847ab68"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Freact-zorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Freact-zorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Freact-zorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/esamattis%2Freact-zorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/esamattis","download_url":"https://codeload.github.com/esamattis/react-zorm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247744332,"owners_count":20988783,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T16:01:10.860Z","updated_at":"2025-04-07T23:08:04.605Z","avatar_url":"https://github.com/esamattis.png","language":"TypeScript","funding_links":[],"categories":["Projects Using Zod"],"sub_categories":[],"readme":"# \u003cimg src=\"https://raw.githubusercontent.com/esamattis/react-zorm/master/react-zorm.svg?sanitize=true\" height=\"60px\"/\u003e React Zorm\n\nType-safe `\u003cform\u003e` for React using [Zod](https://github.com/colinhacks/zod)!\n\nFeatures / opinions\n\n-   🔥 NEW experimental automatic progressive HTML attributes helper\n    -   Docs and [feedback here](https://github.com/esamattis/react-zorm/discussions/48).\n-   💎 Type-safe\n    -   Get form data as a typed object\n    -   Typo-safe `name` and `id` attribute generation\n-   🤯 Simple nested object and array fields\n    -   And still type-safe!\n-   ✅ Validation on the client [and the server](#server-side-validation)\n    -   With [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) or JSON\n    -   Eg. works with any JavaScript backend\n    -   Remix, Next.js, Express, Node.js, CF Workers, Deno etc.\n-   📦 Tiny: Less than 3kb (minified \u0026 gzipped)\n    -   🌳 Tree shakes to be even smaller!\n    -   🤷 No dependencies, only peer deps for React and Zod\n-   🛑 No controlled inputs or context providers required\n    -   ☝️ The form is validated directly from the `\u003cform\u003e` DOM element\n    -   🚀 As performant as React form libraries can get!\n\nIf you enjoy this lib a Twitter shout-out\n[@esamatti](https://twitter.com/esamatti) is always welcome! 😊\n\nYou can also checkout my [talk at React Finland 2022](https://www.youtube.com/watch?v=tCyOdW4D6b8). [Slides](https://docs.google.com/presentation/d/1PEjVuK1vfV_VfJtSnYNHdTUExEUrAURTDALFZZCU2DU/edit?usp=sharing).\n\n## Install\n\n```\nnpm install react-zorm\n```\n\n## Example\n\nAlso on [Codesandbox!](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/signup?file=/src/App.tsx)\n\n```tsx\nimport { z } from \"zod\";\nimport { useZorm } from \"react-zorm\";\n\nconst FormSchema = z.object({\n    name: z.string().min(1),\n    password: z\n        .string()\n        .min(10)\n        .refine((pw) =\u003e /[0-9]/.test(pw), \"Password must contain a number\"),\n});\n\nfunction Signup() {\n    const zo = useZorm(\"signup\", FormSchema, {\n        onValidSubmit(e) {\n            e.preventDefault();\n            alert(\"Form ok!\\n\" + JSON.stringify(e.data, null, 2));\n        },\n    });\n    const disabled = zo.validation?.success === false;\n\n    return (\n        \u003cform ref={zo.ref}\u003e\n            Name:\n            \u003cinput\n                type=\"text\"\n                name={zo.fields.name()}\n                className={zo.errors.name(\"errored\")}\n            /\u003e\n            {zo.errors.name((e) =\u003e (\n                \u003cErrorMessage message={e.message} /\u003e\n            ))}\n            Password:\n            \u003cinput\n                type=\"password\"\n                name={zo.fields.password()}\n                className={zo.errors.password(\"errored\")}\n            /\u003e\n            {zo.errors.password((e) =\u003e (\n                \u003cErrorMessage message={e.message} /\u003e\n            ))}\n            \u003cbutton disabled={disabled} type=\"submit\"\u003e\n                Signup!\n            \u003c/button\u003e\n            \u003cpre\u003eValidation status: {JSON.stringify(zo.validation, null, 2)}\u003c/pre\u003e\n        \u003c/form\u003e\n    );\n}\n```\n\nAlso checkout [this classic TODOs example][todos] demonstrating almost every feature in the library and if you are\nin to Remix checkout [this server-side validation example][remix-example].\n\n## Nested data\n\n### Objects\n\nCreate a Zod type with a nested object\n\n```tsx\nconst FormSchema = z.object({\n    user: z.object({\n        email: z.string().min(1),\n        password: z.string().min(8),\n    }),\n});\n```\n\nand just create the input names with `.user.`:\n\n```tsx\n\u003cinput type=\"text\" name={zo.fields.user.email()} /\u003e;\n\u003cinput type=\"password\" name={zo.fields.user.password()} /\u003e;\n```\n\n### Arrays\n\nArray of user objects for example:\n\n```tsx\nconst FormSchema = z.object({\n    users: z.array(\n        z.object({\n            email: z.string().min(1),\n            password: z.string().min(8),\n        }),\n    ),\n});\n```\n\nand put the array index to `users(index)`:\n\n```tsx\nusers.map((user, index) =\u003e {\n    return (\n        \u003c\u003e\n            \u003cinput type=\"text\" name={zo.fields.users(index).email()} /\u003e\n            \u003cinput type=\"password\" name={zo.fields.users(index).password()} /\u003e\n        \u003c/\u003e\n    );\n});\n```\n\nAnd all this is type checked 👌\n\nSee the [TODOs example][todos] for more details\n\n## Server-side validation\n\nThis is Remix but React Zorm does not actually use any Remix APIs so this method\ncan be adapted for any JavaScript based server.\n\n```tsx\nimport { parseForm } from \"react-zorm\";\n\nexport let action: ActionFunction = async ({ request }) =\u003e {\n    const form = await request.formData();\n    // Get parsed and typed form object. This throws on validation errors.\n    const data = parseForm(FormSchema, form);\n};\n```\n\n### Server-side field errors\n\nThe `useZorm()` hook can take in any additional `ZodIssue`s via the `customIssues` option:\n\n```ts\nconst zo = useZorm(\"signup\", FormSchema, {\n    customIssues: [\n        {\n            code: \"custom\",\n            path: [\"username\"],\n            message: \"The username is already in use\",\n        },\n    ],\n});\n```\n\nThese issues can be generated anywhere. Most commonly on the server. The error\nchain will render these issues on the matching paths just like the errors coming\nfrom the schema.\n\nTo make their generation type-safe react-zorm exports `createCustomIssues()`\nchain to make it easy:\n\n```ts\nconst issues = createCustomIssues(FormSchema);\n\nissues.username(\"Username already in use\");\n\nconst zo = useZorm(\"signup\", FormSchema, {\n    customIssues: issues.toArray(),\n});\n```\n\nThis code is very contrived but take a look at these examples:\n\n-   [Rendering field errors from the Remix Action Functions][remix-example]\n-   [Async validation via React Query](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/async-validation?file=/src/App.tsx)\n\n## The Chains\n\nThe chains are a way to access the form validation state in a type safe way.\nThe invocation via `()` returns the chain value. On the `fields` chain the value is the `name` input attribute\nand the `errors` chain it is the possible ZodIssue object for the field.\n\nThere few other option for invoking the chain:\n\n### `fields` invocation\n\nReturn values for different invocation types\n\n-   `(\"name\"): string` - The `name` attribute value\n-   `(\"id\"): string` - Unique `id` attribute value to be used with labels and `aria-describedby`\n-   `(): string` - The default, same as `\"name\"`\n-   `(index: number): FieldChain` - Special case for setting array indices\n-   `(fn: RenderFunction): any` -\n    Calls the function with `{name: string, id: string, type: ZodType, issues: ZodIssue}` and renders the return value.\n    -   Can be used to create resuable fields. [Codesandbox example](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/render-function?file=/src/App.tsx).\n\n### `errors` invocation\n\n-   `(): ZodIssue | undefined` - Possible ZodIssue object\n-   `(value: T): T | undefined` - Return the passed value on error. Useful for\n    setting class names for example\n-   `(value: typeof Boolean): boolean` - Return `true` when there's an error and `false`\n    when it is ok. Example `.field(Boolean)`.\n-   `\u003cT\u003e(render: (issue: ZodIssue, ...otherIssues: ZodIssue[]) =\u003e T): T | undefined` -\n    Invoke the passed function with the `ZodIssue` and return its return value.\n    When there's no error a `undefined` is returned and the function will not be\n    invoked. Useful for rendering error message components. One field can have\n    multiple issues so to render them all you can use the spread operator\n    `...issues`.\n-   `(index: number): ErrorChain` - Special case for accessing array elements\n\n## Using input values during rendering\n\nThe first tool you should reach is React. Just make the input controlled with\n`useState()`. This works just fine with checkboxes, radio buttons and even with\ntext inputs when the form is small. React Zorm is not really interested how the\ninputs get on the form. It just reads the `value` attributes using the\nplatform form APIs (FormData).\n\nBut if you have a larger form where you need to read the input value and you\nfind it too heavy to read it with just `useState()` you can use `useValue()`\nfrom Zorm.\n\n```ts\nimport { useValue } from \"react-zorm\";\n\nfunction Form() {\n    const zo = useZorm(\"form\", FormSchema);\n    const value = useValue({ zorm: zo, name: zo.fields.input() });\n    return \u003cform ref={zo.ref}\u003e...\u003c/form\u003e;\n}\n```\n\n`useValue()` works by subscribing to the input DOM events and syncing the value\nto a local state. But this does not fix the performance issue yet. You need to\nmove the `useValue()` call to a subcomponent to avoid rendering the whole form\non every input change. See the [Zorm type](#zorm-type) docs on how to do\nthis.\n\nAlternatively you can use the `\u003cValue\u003e` wrapper which allows access to the input\nvalue via render prop:\n\n```ts\nimport { Value } from \"react-zorm\";\n\nfunction Form() {\n    const zo = useZorm(\"form\", FormSchema);\n    return (\n        \u003cform ref={zo.ref}\u003e\n            \u003cinput type=\"text\" name={zo.fields.input()} /\u003e\n            \u003cValue form={zo.ref} name={zo.fields.input()}\u003e\n                {(value) =\u003e \u003cspan\u003eInput value: {value}\u003c/span\u003e}\n            \u003c/Value\u003e\n        \u003c/form\u003e\n    );\n}\n```\n\nThis way only the inner `\u003cspan\u003e` element renders on the input changes.\n\nHere's a\n[codesandox demonstrating](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/use-value?file=/src/App.tsx)\nthese and vizualizing the renders.\n\n## FAQ\n\n### When Zorm validates?\n\nWhen the form submits and on input blurs after the first submit attempt.\n\nIf you want total control over this, pass in `setupListeners: false` and call\n`validate()` manually when you need. Note that now you need to manually prevent\nsubmitting when the form is invalid.\n\n```tsx\nfunction Signup() {\n    const zo = useZorm(\"signup\", FormSchema, { setupListeners: false });\n\n    return (\n        \u003cform\n            ref={zo.ref}\n            onSubmit={(e) =\u003e {\n                const validation = zo.validate();\n\n                if (!validation.success) {\n                    e.preventDefault();\n                }\n            }}\n        \u003e\n            ...\n        \u003c/form\u003e\n    );\n}\n```\n\n### How to handle 3rdparty components?\n\nThat do not create `\u003cinput\u003e` elements?\n\nSince Zorm just works with the native `\u003cform\u003e` you must sync their state to\n`\u003cinput type=\"hidden\"\u003e` elements in order for them to become actually part of\nthe form.\n\nHere's a [Codesandbox example](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/3rdparty?file=/src/App.tsx) with `react-select`.\n\nAnother more modern option is to use the formdata event. [Codesandbox\nexample](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/formdata-event?file=/src/App.tsx)\n\n### How to validate dependent fields like password confirm?\n\nSee \u003chttps://twitter.com/esamatti/status/1488553690613039108\u003e\n\n### How to translate form error messages to other languages?\n\nUse the `ZodIssue`'s `.code` properties to render corresponding error messages\nbased on the current language instead of just rendering the `.message`.\n\nSee this Codesandbox example:\n\n\u003chttps://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/internalization?file=/src/App.tsx\u003e\n\n### How to use checkboxes?\n\nCheckboxes can result to simple booleans or arrays of selected values. These custom Zod types can help with them. See this [usage example](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/checkboxes?file=/src/App.tsx).\n\n```ts\nconst booleanCheckbox = () =\u003e\n    z\n        .string()\n        // Unchecked checkbox is just missing so it must be optional\n        .optional()\n        // Transform the value to boolean\n        .transform(Boolean);\n\nconst arrayCheckbox = () =\u003e\n    z\n        .array(z.string().nullish())\n        .nullish()\n        // Remove all nulls to ensure string[]\n        .transform((a) =\u003e (a ?? []).flatMap((item) =\u003e (item ? item : [])));\n```\n\n### How to do server-side validation without Remix?\n\nIf your server does not support parsing form data to the standard `FormData` you\ncan post the form as JSON and just use `.parse()` from the Zod schema. See the\nnext section for JSON posting.\n\n### How to submit the form as JSON?\n\nPrevent the default submission in `onValidSubmit()` and use `fetch()`:\n\n```ts\nconst zo = useZorm(\"todos\", FormSchema, {\n    onValidSubmit: async (event) =\u003e {\n        event.preventDefault();\n        await fetch(\"/api/form-handler\", {\n            method: \"POST\",\n            headers: {\n                \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify(event.data),\n        });\n    },\n});\n```\n\nIf you need loading states [React Query][react-query] mutations can be cool:\n\n```ts\nimport { useMutation } from \"react-query\";\n\n// ...\n\nconst formPost = useMutation((data) =\u003e {\n    return fetch(\"/api/form-handler\", {\n        headers: {\n            \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(data),\n    });\n});\n\nconst zo = useZorm(\"todos\", FormSchema, {\n    onValidSubmit: async (event) =\u003e {\n        event.preventDefault();\n        formPost.mutate(event.data);\n    },\n});\n\nreturn formPost.isLoading ? \"Sending...\" : null;\n```\n\n[react-query]: https://react-query.tanstack.com/\n\n### How to upload and validate files?\n\nUse `z.instanceof(File)` for the file input type. See [this\nCodesandox](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/file?file=/src/App.tsx:290-317)\nfor an example.\n\nNative forms support files as is but if you need to POST as JSON you can turn\nthe file to a base64 for example. See\n[`FileReader.readAsDataURL()`](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL).\nOr just post the file separately.\n\n## API\n\nTools available for importing from `\"react-zorm\"`\n\n### `useZorm(formName: string, schema: ZodObject, options?: UseZormOptions): Zorm`\n\nCreate a form `Validator`\n\n#### param `formName: string`\n\nThe form name. This used for the input id generation so it should be unique\nstring within your forms.\n\n#### param `schema: ZodObject`\n\nZod schema to parse the form with.\n\n#### param `options?: UseZormOptions`\n\n-   `onValidSubmit(event: ValidSubmitEvent): any`: Called when the form is submitted with valid data\n    -   `ValidSubmitEvent#data`: The Zod parsed form data\n    -   `ValidSubmitEvent#target`: The form HTML Element\n    -   `ValidSubmitEvent#preventDefault()`: Prevent the default form submission\n-   `setupListeners: boolean`: Do not setup any listeners. Ie. `onValidSubmit` won't be\n    called nor the submission is automatically prevented. This gives total control\n    when to validate the form. Set your own `onSubmit` on the form etc. Defaults to `true`.\n-   `customIssues: ZodIssue[]`: Any additional `ZodIssue` to be rendered within\n    the error chain. This is commonly used to handle server-side field validation\n-   `onFormData(event: FormDataEvent)`: Convinience callback for accessing the [formdata\n    event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/formdata_event)\n    because React does not support it directly on the in JSX. This can be used to modify\n    the outgoing form without modifying the form on the DOM. See this [Codesandbox\n    example](https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/formdata-event?file=/src/App.tsx) on how it can used to handle controlled components.\n\n#### return `Zorm`\n\n-   `ref`: A callback ref for the `\u003cform\u003e` element\n-   `form`: The current form element set by the callback ref\n-   `validation: SafeParseReturnType | null`: The current Zod validation status\n    returned by\n    [`safeParse()`][safeparse]\n-   `validate(): SafeParseReturnType`: Manually invoke validation\n-   `fields: FieldChain`: The fields chain\n-   `errors: ErroChain`: The error chain\n\n### `Zorm` Type\n\nThe type of the object returned by `useZorm()`. This type object can be used to\ntype component props if you want to split the form to multiple components and\npass the `zorm` object around.\n\n```ts\nimport type { Zorm } from \"react-zorm\";\n\nfunction MyForm() {\n    const zo = useZorm(\"signup\", FormSchema);\n\n    return (\n        // ...\n        \u003cSubComponent zorm={zo} /\u003e\n        //..\n    );\n}\n\nfunction SubComponent(props: { zorm: Zorm\u003ctypeof FormSchema\u003e }) {\n    // ...\n}\n```\n\n### `useValue(subscription: ValueSubscription): string`\n\nGet live raw value from the input.\n\n#### `ValueSubscription`\n\n-   `form: RefObject\u003cHTMLFormElement\u003e`: The form ref from `zo.ref`\n-   `initialValue: string`: Initial value on the first and ssr render\n-   `transform(value: string): any`: Transform the value before setting it to\n    the internal state. The type can be also changed.\n\n### `Value: React.Component`\n\nRender prop version of the `useValue()` hook. The props are `ValueSubscription`.\nThe render prop child is `(value: string) =\u003e ReactNode`.\n\n```tsx\n\u003cValue zorm={zo} name={zo.fields.input()}\u003e\n    {(value) =\u003e \u003c\u003evalue\u003c/\u003e}\n\u003c/Value\u003e\n```\n\n### `parseForm(schema: ZodObject, form: HTMLFormElement | FormData): Type\u003cZodObject\u003e`\n\nParse `HTMLFormElement` or `FormData` with the given Zod schema.\n\n### `safeParseForm(schema: ZodObject, form: HTMLFormElement | FormData): SafeParseReturnType`\n\nLike `parseForm()` but uses the [`safeParse()`][safeparse] method from Zod.\n\n[todos]: https://codesandbox.io/s/github/esamattis/react-zorm/tree/master/packages/codesandboxes/boxes/todos?file=/src/App.tsx\n[safeparse]: https://github.com/colinhacks/zod/blob/cc8ad1981ba580d1250520fde8878073d4b7d40a/README.md#safeparse\n[remix-example]: https://github.com/esamattis/react-zorm/blob/master/packages/remix-example/app/routes/server-side-validation.tsx\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fesamattis%2Freact-zorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fesamattis%2Freact-zorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fesamattis%2Freact-zorm/lists"}