{"id":13850166,"url":"https://github.com/swan-io/use-form","last_synced_at":"2026-01-12T02:16:15.618Z","repository":{"id":41954444,"uuid":"351386460","full_name":"swan-io/use-form","owner":"swan-io","description":"A simple, fast, and opinionated form library for React \u0026 React Native focusing on UX.","archived":false,"fork":false,"pushed_at":"2025-07-21T09:26:11.000Z","size":10125,"stargazers_count":213,"open_issues_count":1,"forks_count":7,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-11-12T13:07:05.649Z","etag":null,"topics":["form","react","validation"],"latest_commit_sha":null,"homepage":"https://swan-io.github.io/use-form","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/swan-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"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,"zenodo":null}},"created_at":"2021-03-25T09:56:44.000Z","updated_at":"2025-10-27T13:56:51.000Z","dependencies_parsed_at":"2023-02-17T06:30:34.560Z","dependency_job_id":"3cae12ce-82fa-474c-a852-d95f6549f527","html_url":"https://github.com/swan-io/use-form","commit_stats":{"total_commits":209,"total_committers":4,"mean_commits":52.25,"dds":0.09569377990430628,"last_synced_commit":"5f7509b0d85814e5c867e2266b37992ba9141c8e"},"previous_names":["swan-io/use-form","swan-io/react-ux-form"],"tags_count":35,"template":false,"template_full_name":null,"purl":"pkg:github/swan-io/use-form","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swan-io%2Fuse-form","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swan-io%2Fuse-form/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swan-io%2Fuse-form/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swan-io%2Fuse-form/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/swan-io","download_url":"https://codeload.github.com/swan-io/use-form/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swan-io%2Fuse-form/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28331857,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-12T00:36:25.062Z","status":"online","status_checked_at":"2026-01-12T02:00:08.677Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["form","react","validation"],"created_at":"2024-08-04T20:01:00.466Z","updated_at":"2026-01-12T02:16:15.612Z","avatar_url":"https://github.com/swan-io.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# @swan-io/use-form\n\n[![mit licence](https://img.shields.io/dub/l/vibe-d.svg?style=for-the-badge)](https://github.com/swan-io/use-form/blob/main/LICENSE)\n[![npm version](https://img.shields.io/npm/v/@swan-io/use-form?style=for-the-badge)](https://www.npmjs.org/package/@swan-io/use-form)\n[![bundlephobia](https://img.shields.io/bundlephobia/minzip/@swan-io/use-form?label=size\u0026style=for-the-badge)](https://bundlephobia.com/result?p=@swan-io/use-form)\n\nA simple, fast, and opinionated form library for React \u0026 React Native focusing on UX.\u003cbr\u003e\n👉 Take a look at [the demo website](https://swan-io.github.io/use-form).\n\n## Setup\n\n```bash\n$ npm install --save @swan-io/use-form\n# --- or ---\n$ yarn add @swan-io/use-form\n```\n\n## Features\n\n- Subscription-based field updates (avoid re-render the whole form on each keystroke 🔥)\n- Validation strategies ✨\n- Field sanitization\n- Mounted-only fields validation\n- Advanced focus handling\n- Best-in-class TypeScript support\n- Sync and async form submission\n\n## Motivation\n\nWhy another React form library 🤔?\u003cbr\u003e\nBecause, as silly as it seems, we couldn't find any existing library which fits our existing needs:\n\n- We want validation strategies per field because we fell in love with them when we read the [re-formality](https://github.com/MinimaHQ/re-formality) documentation (which is unfortunately only available for [ReScript](https://rescript-lang.org/)).\n- It should be able to handle huge forms without a single performance hiccup.\n- Validation should be simple, reusable, and testable (aka just functions).\n- It shouldn't even try to validate unmounted fields.\n- It should have built-in focus management (to improve the keyboard flow of our React Native forms).\n\n## Validation strategies ✨\n\nThe key of **good UX** is simple: validation should be executed **in continue**, feedback should be provided **when it makes sense**.\n\n### Quick example: A credit card field 💳\n\nLet's say we want to display a valid state icon (✔) when the input value is a valid credit card number but don't want to display an error until the user blurs the field (and lets the value in an invalid state).\n\n#### Something like this:\n\n![Valid credit card](docs/credit-card-valid.gif)\n![Invalid credit card](docs/credit-card-error.gif)\n\nHow do we easily achieve such magic? With the `onSuccessOrBlur` strategy 🧙‍♂️\u003cbr\u003e\n\n```tsx\nconst {} = useForm({\n  cardNumber: { initialValue: \"\", strategy: \"onSuccessOrBlur\" },\n});\n```\n\nOf course, `onSuccessOrBlur` will not fit perfectly every use-case!\u003cbr\u003e\nThat's precisely why every field config could declare its own `strategy`:\n\n| Strategy          | When feedback will be available?                              |\n| ----------------- | ------------------------------------------------------------- |\n| `onChange`        | On first change (as the user types or update the value)       |\n| `onSuccess`       | On first validation success                                   |\n| `onBlur`          | On first field blur                                           |\n| `onSuccessOrBlur` | On first validation success or first field blur **(default)** |\n| `onSubmit`        | On form submit                                                |\n\n#### Note that:\n\n- The strategies will only be activated after the field value update / the form submission.\n- Once the first feedback is given (the field is `valid` or should display an `error` message), the field switches to what we call _\"talkative\"_ state. After that, feedback will be updated on each value change until this field or the form is reset.\n\n## API\n\n⚠️ The API is described using TypeScript pseudocode.\u003cbr\u003eThese types are not exported by the library / are not even always valid.\n\n### useForm\n\n`useForm` takes one argument (a map of your fields configs) and returns a set of helpers (functions, components, and values) to manage your form state.\n\n```tsx\nimport { useForm } from \"@swan-io/use-form\";\n\nconst {\n  formStatus,\n  Field,\n  FieldsListener,\n  getFieldValue,\n  getFieldRef,\n  setFieldValue,\n  setFieldError,\n  focusField,\n  resetField,\n  sanitizeField,\n  validateField,\n  listenFields,\n  resetForm,\n  submitForm,\n} = useForm({\n  // Keys are used as fields names\n  fieldName: {\n    initialValue: \"\",\n    // Properties below are optional (those are the default values)\n    strategy: \"onSuccessOrBlur\",\n    isEqual: (value1, value2) =\u003e Object.is(value1, value2),\n    sanitize: (value) =\u003e value,\n    validate: (value, { focusField, getFieldValue }) =\u003e {},\n  },\n});\n```\n\n#### Field config\n\n```tsx\ntype fieldConfig = {\n  // The initial field value. It could be anything (string, number, boolean…)\n  initialValue: Value;\n\n  // The chosen strategy. See \"validation strategies\" paragraph\n  strategy: Strategy;\n\n  // Used to perform initial and current value comparison\n  isEqual: (value1: Value, value2: Value) =\u003e boolean;\n\n  // Will be run on value before validation and submission. Useful from trimming whitespaces\n  sanitize: (value: Value) =\u003e Value;\n\n  // Used to perform field validation. It could return an error message (or nothing)\n  validate: (value: Value) =\u003e ErrorMessage | void;\n};\n```\n\n#### formStatus\n\n```tsx\ntype formStatus =\n  | \"untouched\" // no field has been updated\n  | \"editing\"\n  | \"submitting\"\n  | \"submitted\";\n```\n\n#### `\u003cField /\u003e`\n\nA component that exposes everything you need locally as a `children` render prop.\n\n```tsx\n\u003cField name=\"fieldName\"\u003e\n  {\n    (props: {\n      // A ref to pass to your element (only required for focus handling)\n      ref: MutableRefObject;\n      // The field value\n      value: Value;\n      // Is the field valid?\n      valid: boolean;\n      // The field is invalid: here its error message.\n      error?: ErrorMessage;\n      // The onBlur handler (required for onBlur and onSuccessOrBlur strategies)\n      onBlur: () =\u003e void;\n      // The onChange handler (required)\n      onChange: (value: Value) =\u003e void;\n    }) =\u003e /* … */\n  }\n\u003c/Field\u003e\n```\n\n#### `\u003cFieldsListener /\u003e`\n\nA component that listens for fields states changes. It's useful when a part of your component needs to react to fields updates without triggering a full re-render.\n\n```tsx\n\u003cFieldsListener names={[\"firstName\", \"lastName\"]}\u003e\n  {\n    (states: Record\u003c\"firstName\" | \"lastName\", {\n      // The field value\n      value: Value;\n      // Is the field valid?\n      valid: boolean;\n      // The field is invalid: here its error message.\n      error?: ErrorMessage;\n    }\u003e) =\u003e /* … */\n  }\n\u003c/FieldsListener\u003e\n```\n\n#### getFieldValue\n\nBy setting `sanitize: true`, you will enforce sanitization.\n\n```tsx\ntype getFieldValue = (\n  name: FieldName,\n  options?: {\n    sanitize?: boolean;\n  },\n) =\u003e Value;\n```\n\n#### getFieldRef\n\nReturn the field stable `ref`.\n\n```tsx\ntype getFieldRef = \u003cT\u003e(name: FieldName) =\u003e MutableRefObject\u003cT\u003e;\n```\n\n#### setFieldValue\n\nBy setting `validate: true`, you will enforce validation. It has no effect if the field is already _talkative_.\n\n```tsx\ntype setFieldValue = (\n  name: FieldName,\n  value: Value,\n  options?: {\n    validate?: boolean;\n  },\n) =\u003e void;\n```\n\n#### setFieldError\n\nWill make the field _talkative_.\n\n```tsx\ntype setFieldError = (name: FieldName, error?: ErrorMessage) =\u003e void;\n```\n\n#### focusField\n\nWill only work if you forward the `Field` provided `ref` to your input.\n\n```tsx\ntype focusField = (name: FieldName) =\u003e void;\n```\n\n#### resetField\n\nHide user feedback (the field is not _talkative_ anymore) and set value to `initialValue`.\n\n```tsx\ntype resetField = (name: FieldName) =\u003e void;\n```\n\n#### sanitizeField\n\nSanitize the field value.\n\n```tsx\ntype sanitizeField = (name: FieldName) =\u003e void;\n```\n\n#### validateField\n\nOnce you manually call validation, the field automatically switches to _talkative_ state.\n\n```tsx\ntype validateField = (name: FieldName) =\u003e ErrorMessage | void;\n```\n\n#### listenFields\n\nA function that listen for fields states changes. Useful when you want to apply side effects on values change.\n\n```tsx\nReact.useEffect(() =\u003e {\n  const removeListener = listenFields(\n    [\"firstName\", \"lastName\"],\n    (states: Record\u003c\"firstName\" | \"lastName\", {\n      // The field value\n      value: Value;\n      // Is the field valid?\n      valid: boolean;\n      // The field is invalid: here its error message.\n      error?: ErrorMessage;\n    }\u003e) =\u003e /* … */\n  );\n\n  return () =\u003e {\n    removeListener();\n  }\n}, []);\n```\n\n#### resetForm\n\nHide user feedback for all fields (they are not _talkative_ anymore). Reset values to their corresponding `initialValue` and `formStatus` to `untouched`.\n\n```tsx\ntype resetForm = () =\u003e void;\n```\n\n#### submitForm\n\nSubmit your form. Each callback could return a `Promise` to keep `formStatus` in `submitting` state.\n\n```tsx\ntype submitForm = (options?: {\n  onSuccess?: (values: OptionRecord\u003cValues\u003e) =\u003e Future\u003cunknown\u003e | Promise\u003cunknown\u003e | void;\n  onFailure?: (errors: Partial\u003cRecord\u003ckeyof Values, ErrorMessage\u003e\u003e) =\u003e void;\n  // by default, it will try to focus the first errored field (which is a good practice)\n  focusOnFirstError?: boolean;\n}) =\u003e void;\n```\n\n### combineValidators\n\nAs it's a very common case to use several validation functions per field, we export a `combineValidators` helper function that allows you to chain sync validation functions: it will run them sequentially until an error is returned.\n\n```tsx\nimport { combineValidators, useForm } from \"@swan-io/use-form\";\n\nconst validateRequired = (value: string) =\u003e {\n  if (!value) {\n    return \"required\";\n  }\n};\n\nconst validateEmail = (email: string) =\u003e {\n  if (!/.+@.+\\..{2,}/.test(email)) {\n    return \"invalid email\";\n  }\n};\n\nconst MyAwesomeForm = () =\u003e {\n  const { Field, submitForm } = useForm({\n    emailAddress: {\n      initialValue: \"\",\n      // will run each validation function until an error is returned\n      validate: combineValidators(\n        isEmailRequired \u0026\u0026 validateRequired, // validation checks could be applied conditionally\n        validateEmail,\n      ),\n    },\n  });\n\n  // …\n};\n```\n\n### toOptionalValidator\n\nVery often, we want to execute validation only if a value is not empty. By wrapping any validator (or combined validators) with `toOptionalValidator`, you can bypass the validation in such cases.\n\n```tsx\nimport { toOptionalValidator, Validator } from \"@swan-io/use-form\";\n\n// This validator will error if the string length is \u003c 3 (even if it's an empty string)\nconst validator: Validator\u003cstring\u003e = (value) =\u003e {\n  if (value.length \u003c 3) {\n    return \"Must be at least 3 characters\";\n  }\n};\n\n// This validator will error if value is not empty string and if the string length is \u003c 3\nconst optionalValidator = toOptionalValidator(validator);\n```\n\nThis function also accept a second param (required for non-string validators) to specify what is an empty value.\n\n```tsx\nimport { toOptionalValidator, Validator } from \"@swan-io/use-form\";\n\nconst validator: Validator\u003cnumber\u003e = (value) =\u003e {\n  if (value \u003c 10) {\n    return \"Must pick at least 10 items\";\n  }\n};\n\n// This validator will also accept a value of 0, as we consider it \"empty\"\nconst optionalValidator = toOptionalValidator(validator, (value) =\u003e value === 0);\n```\n\n## Quickstart\n\n```tsx\nimport { useForm } from \"@swan-io/use-form\";\n\nconst MyAwesomeForm = () =\u003e {\n  const { Field, submitForm } = useForm({\n    firstName: {\n      initialValue: \"\",\n      strategy: \"onSuccessOrBlur\",\n      sanitize: (value) =\u003e value.trim(), // we trim value before validation and submission\n      validate: (value) =\u003e {\n        if (value === \"\") {\n          return \"First name is required\";\n        }\n      },\n    },\n  });\n\n  return (\n    \u003cform\n      onSubmit={(event: React.FormEvent) =\u003e {\n        event.preventDefault();\n\n        submitForm({\n          onSuccess: (values) =\u003e console.log(\"values\", values), // all fields are valid\n          onFailure: (errors) =\u003e console.log(\"errors\", errors), // at least one field is invalid\n        });\n      }}\n    \u003e\n      \u003cField name=\"firstName\"\u003e\n        {({ error, onBlur, onChange, valid, value }) =\u003e (\n          \u003c\u003e\n            \u003clabel htmlFor=\"firstName\"\u003eFirst name\u003c/label\u003e\n\n            \u003cinput\n              id=\"firstName\"\n              onBlur={onBlur}\n              value={value}\n              onChange={({ target }) =\u003e {\n                onChange(target.value);\n              }}\n            /\u003e\n\n            {valid \u0026\u0026 \u003cspan\u003eValid\u003c/span\u003e}\n            {error \u0026\u0026 \u003cspan\u003eInvalid\u003c/span\u003e}\n          \u003c/\u003e\n        )}\n      \u003c/Field\u003e\n\n      \u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n    \u003c/form\u003e\n  );\n};\n```\n\n## More examples\n\nA full set of examples is available on [the demo website](https://swan-io.github.io/use-form) or in the [`/website` directory](https://github.com/swan-io/use-form/tree/main/website) project. Just clone the repository, install its dependencies and start it!\n\n## Acknowledgements\n\n- [re-formality](https://github.com/MinimaHQ/re-formality) for the [validation strategies](https://github.com/MinimaHQ/re-formality/blob/master/docs/02-ValidationStrategies.md) idea.\n- [react-hook-form](https://react-hook-form.com/) and [react-final-form](https://github.com/final-form/react-final-form) for their subscription pattern implementations.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswan-io%2Fuse-form","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fswan-io%2Fuse-form","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswan-io%2Fuse-form/lists"}