Repository: Code-Forge-Net/remix-hook-form Branch: main Commit: 43bfbecae70d Files: 38 Total size: 109.8 KB Directory structure: gitextract__hdis4zo/ ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── publish-commit.yaml │ ├── publish.yaml │ └── validate.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.MD ├── package.json ├── pull_request_template.md ├── src/ │ ├── hook/ │ │ ├── index.test.tsx │ │ └── index.tsx │ ├── index.ts │ ├── middleware/ │ │ └── index.ts │ └── utilities/ │ ├── index.test.ts │ └── index.ts ├── test-apps/ │ └── react-router/ │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── Dockerfile.bun │ ├── Dockerfile.pnpm │ ├── README.md │ ├── app/ │ │ ├── app.css │ │ ├── root.tsx │ │ ├── routes/ │ │ │ └── home.tsx │ │ ├── routes.ts │ │ └── welcome/ │ │ └── welcome.tsx │ ├── package.json │ ├── react-router.config.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── tsconfig.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "prettier" ], "ignorePatterns": ["node_modules/", "testing-app"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": ["@typescript-eslint", "react", "prettier"], "rules": { "@typescript-eslint/no-unnecessary-type-constraint": "off", "@typescript-eslint/no-explicit-any": "off", "no-console": "error" }, "settings": { "react": { "version": "18" } } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [AlemTuzlak] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/workflows/publish-commit.yaml ================================================ name: 🚀 pkg-pr-new on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - run: corepack enable - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm install - name: Build run: npm run build - run: npx pkg-pr-new publish ================================================ FILE: .github/workflows/publish.yaml ================================================ name: Publish Package to npmjs on: release: types: [published] workflow_dispatch: jobs: npm-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Setup .npmrc file to publish to npm - uses: actions/setup-node@v4 with: node-version: "20.x" registry-url: "https://registry.npmjs.org" - run: npm ci - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/validate.yaml ================================================ name: 🚀 Validation Pipeline concurrency: group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: push: branches: [main] pull_request: branches: [main] permissions: actions: write contents: read # Required to put a comment into the pull-request pull-requests: write jobs: # lint: # name: ⬣ Biome lint # runs-on: ubuntu-latest # steps: # - name: ⬇️ Checkout repo # uses: actions/checkout@v4 # - name: Setup Biome # uses: biomejs/setup-biome@v2 # - name: Run Biome # run: biome ci . typecheck: name: 🔎 Type check runs-on: ubuntu-latest steps: - name: 🛑 Cancel Previous Runs uses: styfle/cancel-workflow-action@0.12.1 - name: ⬇️ Checkout repo uses: actions/checkout@v4 - name: ⎔ Setup node uses: actions/setup-node@v4 with: node-version: 20 - name: 📥 Download deps uses: bahmutov/npm-install@v1 with: useLockFile: false - name: 🔎 Type check run: npm run typecheck vitest: name: ⚡ Unit Tests runs-on: ubuntu-latest steps: - name: 🛑 Cancel Previous Runs uses: styfle/cancel-workflow-action@0.12.1 - name: ⬇️ Checkout repo uses: actions/checkout@v4 - name: ⎔ Setup node uses: actions/setup-node@v4 with: node-version: 20 - name: 📥 Download deps uses: bahmutov/npm-install@v1 with: useLockFile: false - name: Install dotenv cli run: npm install -g dotenv-cli - name: ⚡ Run vitest run: npm run test # - name: "Report Coverage" # Only works if you set `reportOnFailure: true` in your vite config as specified above # if: always() # uses: davelosert/vitest-coverage-report-action@v2 ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and *not* Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port /build .history .react-router .turbo ================================================ FILE: .prettierignore ================================================ node_modules /build ================================================ FILE: .prettierrc ================================================ { } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Code Forge Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # remix-hook-form ![GitHub Repo stars](https://img.shields.io/github/stars/forge-42/remix-hook-form?style=social) ![npm](https://img.shields.io/npm/v/remix-hook-form?style=plastic) ![GitHub](https://img.shields.io/github/license/forge-42/remix-hook-form?style=plastic) ![npm](https://img.shields.io/npm/dy/remix-hook-form?style=plastic) ![npm](https://img.shields.io/npm/dw/remix-hook-form?style=plastic) ![GitHub top language](https://img.shields.io/github/languages/top/forge-42/remix-hook-form?style=plastic) Remix-hook-form is a powerful and lightweight wrapper around [react-hook-form](https://react-hook-form.com/) that streamlines the process of working with forms and form data in your [React Router](https://reactrouter.com) applications. With a comprehensive set of hooks and utilities, you'll be able to easily leverage the flexibility of react-hook-form without the headache of boilerplate code. And the best part? Remix-hook-form has zero dependencies, making it easy to integrate into your existing projects and workflows. Say goodbye to bloated dependencies and hello to a cleaner, more efficient development process with Remix-hook-form. Oh, and did we mention that this is fully Progressively enhanced? That's right, you can use this with or without javascript! ## Remix.run support Versions older than 6.0.0 are compatible with [Remix.run](https://remix.run) applications. If you are using Remix.run, please use version 5.1.1 or lower. ## Install ```bash npm install remix-hook-form react-hook-form ``` ## Basic usage Here is an example usage of remix-hook-form. It will work with **and without** JS. Before running the example, ensure to install additional dependencies: ```bash npm install zod @hookform/resolvers ``` ```ts import { useRemixForm, getValidatedFormData } from "remix-hook-form"; import { Form } from "react-router"; import { zodResolver } from "@hookform/resolvers/zod"; import * as zod from "zod"; import type { Route } from "./+types/home"; const schema = zod.object({ name: zod.string().min(1), email: zod.string().email().min(1), }); type FormData = zod.infer; const resolver = zodResolver(schema); export const action = async ({ request }: Route.ActionArgs) => { const { errors, data, receivedValues: defaultValues } = await getValidatedFormData(request, resolver); if (errors) { // The keys "errors" and "defaultValues" are picked up automatically by useRemixForm return { errors, defaultValues }; } // Do something with the data return data; }; export default function MyForm() { const { handleSubmit, formState: { errors }, register, } = useRemixForm({ mode: "onSubmit", resolver, }); return (
); } ``` ## Serialization of values client => server By default, all values are serialized to strings before being sent to the server. This is because that is how form data works, it only accepts strings, nulls or files, this means that even strings would get "double stringified" and become strings like this: ```ts const string = "'123'"; ``` This helps with the fact that validation on the server can't know if your stringified values received from the client are actually strings or numbers or dates or whatever. For example, if you send this formData to the server: ```ts const formData = { name: "123", age: 30, hobbies: ["Reading", "Writing", "Coding"], boolean: true, a: null, // this gets omitted because it's undefined b: undefined, numbers: [1, 2, 3], other: { skills: ["testing", "testing"], something: "else", }, }; ``` It would be sent to the server as: ```ts { name: "123", age: "30", hobbies: "[\"Reading\",\"Writing\",\"Coding\"]", boolean: "true", a: "null", numbers: "[1,2,3]", other: "{\"skills\":[\"testing\",\"testing\"],\"something\":\"else\"}", } ``` Then the server does not know if the `name` property used to be a string or a number, your validation schema would fail if it parsed it back to a number and you expected it to be a string. Conversely, if you didn't parse the rest of this data you wouldn't have objects, arrays etc. but strings. The double stringification helps with this as it would correctly parse the data back to the original types, but it also means that you have to use the helpers provided by this package to parse the data back to the original types. This is the default behavior, but you can change this behavior by setting the `stringifyAllValues` prop to `false` in the `useRemixForm` hook. ```ts const { handleSubmit, formState, register } = useRemixForm({ mode: "onSubmit", resolver, stringifyAllValues: false, }); ``` This only affects strings really as it either double stringifies them or it doesn't. The bigger impact of all of this is on the server side. By default all the server helpers expect the data to be double stringified which allows the utils to parse the data back to the original types easily. If you don't want to double stringify the data then you can set the `preserveStringified` prop to `true` in the `getValidatedFormData` function. ```ts // Third argument is preserveStringified and is false by default const { errors, data } = await getValidatedFormData(request, resolver, true); ``` Because the data by default is double stringified the data returned by the util and sent to your validator would look like this: ```ts const data = { name: "123", age: 30, hobbies: ["Reading", "Writing", "Coding"], boolean: true, a: null, // this gets omitted because it's undefined b: undefined, numbers: [1, 2, 3], other: { skills: ["testing", "testing"], something: "else", }, }; ``` If you set `preserveStringified` to `true` then the data would look like this: ```ts const data = { name: "123", age: "30", hobbies: ["Reading", "Writing", "Coding"], boolean: "true", a: "null", numbers: ["1","2","3"], other: { skills: ["testing", "testing"], something: "else", }, }; ``` This means that your validator would have to handle all the type conversions and validations for all the different types of data. This is a lot of work and it's not worth it usually, the best place to use this approach if you store the info in searchParams. If you want to handle it like this what you can do is use something like `coerce` from `zod` to convert the data to the correct type before checking it. ```ts import { z } from "zod"; const formDataZodSchema = z.object({ name: z.string().min(1), // converts the string to a number age: z.coerce.number().int().positive(), }); type SchemaFormData = z.infer; const resolver = zodResolver(formDataZodSchema); export const action = async ({ request }: Route.ActionArgs) => { const { errors, data } = await getValidatedFormData( request, resolver, true, ); if (errors) { return { errors }; } // Do something with the data }; ``` ## Fetcher usage You can pass in a fetcher as an optional prop and `useRemixForm` will use that fetcher to submit the data and read the errors instead of the default behavior. For more info see the docs on `useRemixForm` below. ## Video example and tutorial If you wish to learn in depth on how form handling works in React router/Remix.run and want an example using this package I have prepared a video tutorial on how to do it. It's a bit long but it covers everything you need to know about form handling in React Router/Remix. It also covers how to use this package. You can find it here: https://youtu.be/iom5nnj29sY?si=l52WRE2bqpkS2QUh ## Middleware mode From v7 you can use middleware to extract the form data and access it anywhere in your actions and loaders. All you have to do is set it up in your `root.tsx` file like this: ```ts import { unstable_extractFormDataMiddleware } from "remix-hook-form/middleware"; export const unstable_middleware = [unstable_extractFormDataMiddleware()]; ``` And then access it in your actions and loaders like this: ```ts import { getFormData, getValidatedFormData } from "remix-hook-form/middleware"; export const loader = async ({ context }: LoaderFunctionArgs) => { const searchParamsFormData = await getFormData(context); return { result: "success" }; }; export const action = async ({ context }: ActionFunctionArgs) => { // OR: const formData = await getFormData(context); const { data, errors, receivedValues } = await getValidatedFormData( context, resolver, ); if (errors) { return { errors, receivedValues }; } return { result: "success" }; }; ``` ## API's ### getValidatedFormData Now supports no-js form submissions! If you made a GET request instead of a POST request and you are using this inside of a loader it will try to extract the data from the search params If the form is submitted without js it will try to parse the formData object and covert it to the same format as the data object returned by `useRemixForm`. If the form is submitted with js it will automatically extract the data from the request object and validate it. getValidatedFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request/formData object and the resolver function. It returns an object with three properties: `errors`, `receivedValues` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`. The `receivedValues` property allows you to set the default values of your form to the values that were received from the request object. This is useful if you want to display the form again with the values that were submitted by the user when there is no JS present #### Example with errors only If you don't want the form to persist submitted values in the case of validation errors then you can just return the `errors` object directly from the action. ```jsx /** all the same code from above */ export const action = async ({ request }: Route.ActionArgs) => { // Takes the request from the frontend, parses and validates it and returns the data const { errors, data } = await getValidatedFormData(request, resolver); if (errors) { return { errors }; } // Do something with the data }; ``` #### Example with errors and receivedValues If your action returrns `defaultValues` key then it will be automatically used by `useRemixForm` to populate the default values of the form. ```jsx /** all the same code from above */ export const action = async ({ request }: Route.ActionArgs) => { // Takes the request from the frontend, parses and validates it and returns the data const { errors, data, receivedValues: defaultValues } = await getValidatedFormData(request, resolver); if (errors) { return { errors, defaultValues }; } // Do something with the data }; ``` ### validateFormData validateFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the formData object and the resolver function. It returns an object with two properties: `errors` and `data`. If there are no errors, `errors` will be `undefined`. If there are errors, `errors` will be an object with the same shape as the `errors` object returned by `useRemixForm`. If there are no errors, `data` will be an object with the same shape as the `data` object returned by `useRemixForm`. The difference between `validateFormData` and `getValidatedFormData` is that `validateFormData` only validates the data while the `getValidatedFormData` function also extracts the data automatically from the request object assuming you were using the default setup. ```jsx /** all the same code from above */ export const action = async ({ request }: Route.ActionArgs) => { // Lets assume you get the data in a different way here but still want to validate it const formData = await yourWayOfGettingFormData(request); // Takes the request from the frontend, parses and validates it and returns the data const { errors, data } = await validateFormData(formData, resolver); if (errors) { return { errors }; } // Do something with the data }; ``` ### createFormData createFormData is a utility function that can be used to create a FormData object from the data returned by the handleSubmit function from `react-hook-form`. It takes one argument, the `data` from the `handleSubmit` function and it converts everything it can to strings and appends files as well. It returns a FormData object. ```jsx /** all the same code from above */ export default function MyForm() { const { ... } = useRemixForm({ ..., submitHandlers: { onValid: data => { // This will create a FormData instance ready to be sent to the server, by default all your data is converted to a string before sent const formData = createFormData(data); // Do something with the formData } } }); return ( ... ); } ``` ### parseFormData parseFormData is a utility function that can be used to parse the data submitted to the action by the handleSubmit function from `react-hook-form`. It takes two arguments, first one is the `request` submitted from the frontend and the second one is `preserveStringified`, the form data you submit will be cast to strings because that is how form data works, when retrieving it you can either keep everything as strings or let the helper try to parse it back to original types (eg number to string), default is `false`. It returns an object that contains unvalidated `data` submitted from the frontend. ```jsx /** all the same code from above */ export const action = async ({ request }: Route.ActionArgs) => { // Allows you to get the data from the request object without having to validate it const formData = await parseFormData(request); // formData.age will be a number const formDataStringified = await parseFormData(request, true); // formDataStringified.age will be a string // Do something with the data }; ``` ### getFormDataFromSearchParams If you're using a GET request formData is not available on the request so you can use this method to extract your formData from the search parameters assuming you set all your data in the search parameters ## Hooks ### useRemixForm `useRemixForm` is a hook that can be used to create a form in your React Router / Remix application. It's basically the same as react-hook-form's [`useForm`](https://www.react-hook-form.com/api/useform/) hook, with the following differences: **Additional options** - `submitHandlers`: an object containing two properties: - `onValid`: can be passed into the function to override the default behavior of the `handleSubmit` success case provided by the hook. If you need to pass additional data not tracked in the form, you'll need to manually merge it here with your form data to be submitted, as the `submitData` hook option is ignored in this case. - `onInvalid`: can be passed into the function to override the default behavior of the `handleSubmit` error case provided by the hook. - `submitConfig`: allows you to pass additional configuration to the `useSubmit` function from React Router / Remix, such as `{ replace: true }` to replace the current history entry instead of pushing a new one. The `submitConfig` trumps `Form` props from React Router / Remix. The following props will be used from `Form` if no submitConfig is provided: - `method` - `action` - `encType` - `submitData`: allows you to pass additional data to the backend when the form is submitted. - `fetcher`: if provided then this fetcher will be used to submit data and get a response (errors / defaultValues) instead of React Router/Remix's `useSubmit` and `useActionData` hooks. **`register` will respect default values returned from the action** If the React Router/Remix hook `useActionData` returns an object with `defaultValues` these will automatically be used as the default value when calling the `register` function. This is useful when the form has errors and you want to persist the values when JS is not enabled. If a `fetcher` is provided default values will be read from the fetcher's data. **`handleSubmit`** The returned `handleSubmit` function does two additional things - The success case is provided by default where when the form is validated by the provided resolver, and it has no errors, it will automatically submit the form to the current route using a POST request. The data will be sent as `formData` to the action function. - The data that is sent is automatically wrapped into a formData object and passed to the server ready to be used. Easiest way to consume it is by using the `parseFormData` or `getValidatedFormData` function from the `remix-hook-form` package. **`formState.errors`** The `errors` object inside `formState` is automatically populated with the errors returned by the action. If the action returns an `errors` key in it's data then that value will be used to populate errors, otherwise the whole action response is assumed to be the errors object. If a `fetcher` is provided then errors are read from the fetcher's data. #### Examples **Overriding the default onValid and onInvalid cases** ```jsx const { ... } = useRemixForm({ ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM, submitHandlers: { onValid: data => { // Do something with the formData }, onInvalid: errors => { // Do something with the errors } } }); ``` **Overriding the submit from remix to do something else** ```jsx const { ... } = useRemixForm({ ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM, submitConfig: { replace: true, method: "PUT", action: "/api/youraction", }, }); ``` **Passing additional data to the backend** ```jsx const { ... } = useRemixForm({ ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM, submitData: { someFieldsOutsideTheForm: "someValue" }, }); // or if customizing with `onValid` handler const { ... } = useRemixForm({ ...ALL_THE_SAME_CONFIG_AS_REACT_HOOK_FORM, submitHandlers: { onValid: data => { const mergedData = { ...data, someFieldsOutsideTheForm: "someValue" } const formData = createFormData(mergedData); // ... submit } } }); ``` ### RemixFormProvider Identical to the [`FormProvider`](https://react-hook-form.com/api/formprovider/) from `react-hook-form`, but it also returns the changed `formState.errors` and `handleSubmit` object. ```jsx export default function Form() { const methods = useRemixForm(); return ( // pass all methods into the context
); } ================================================ FILE: test-apps/react-router/app/routes.ts ================================================ import { type RouteConfig, index } from "@react-router/dev/routes"; export default [index("routes/home.tsx")] satisfies RouteConfig; ================================================ FILE: test-apps/react-router/app/welcome/welcome.tsx ================================================ import logoDark from "./logo-dark.svg"; import logoLight from "./logo-light.svg"; export function Welcome() { return (
React Router React Router
); } const resources = [ { href: "https://reactrouter.com/docs", text: "React Router Docs", icon: ( ), }, { href: "https://rmx.as/discord", text: "Join Discord", icon: ( ), }, ]; ================================================ FILE: test-apps/react-router/package.json ================================================ { "name": "react-router-app", "private": true, "type": "module", "scripts": { "build": "react-router build", "dev": "react-router dev", "start": "react-router-serve ./build/server/index.js", "typecheck": "react-router typegen && tsc --build --noEmit" }, "dependencies": { "@react-router/node": "^7.5.0", "@react-router/serve": "^7.5.0", "isbot": "^5.1.17", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.5.0", "react-hook-form": "7.55.0", "remix-hook-form": "*" }, "devDependencies": { "@react-router/dev": "^7.5.0", "@types/node": "^20", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "autoprefixer": "^10.4.20", "postcss": "^8.4.49", "tailwindcss": "^3.4.15", "typescript": "^5.6.3", "vite": "^5.4.11", "vite-tsconfig-paths": "^5.1.2", "react-router-devtools": "^1.1.9" } } ================================================ FILE: test-apps/react-router/react-router.config.ts ================================================ import type { Config } from "@react-router/dev/config"; declare module "react-router" { interface Future { unstable_middleware: true; // 👈 Enable middleware types } } export default { future: { unstable_middleware: true, // 👈 Enable middleware }, } satisfies Config; ================================================ FILE: test-apps/react-router/tailwind.config.ts ================================================ import type { Config } from "tailwindcss"; export default { content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], theme: { extend: { fontFamily: { sans: [ '"Inter"', "ui-sans-serif", "system-ui", "sans-serif", '"Apple Color Emoji"', '"Segoe UI Emoji"', '"Segoe UI Symbol"', '"Noto Color Emoji"', ], }, }, }, plugins: [], } satisfies Config; ================================================ FILE: test-apps/react-router/tsconfig.json ================================================ { "include": [ "**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*" ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "types": ["node", "vite/client"], "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", "jsx": "react-jsx", "rootDirs": [".", "./.react-router/types"], "baseUrl": ".", "paths": { "~/*": ["./app/*"] }, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "noEmit": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true } } ================================================ FILE: test-apps/react-router/vite.config.ts ================================================ import { reactRouter } from "@react-router/dev/vite"; import autoprefixer from "autoprefixer"; import { reactRouterDevTools } from "react-router-devtools"; import tailwindcss from "tailwindcss"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ css: { postcss: { plugins: [tailwindcss, autoprefixer], }, }, plugins: [reactRouterDevTools(), reactRouter(), tsconfigPaths()], }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Projects */ // "incremental": true, /* Enable incremental compilation */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "es2018" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "lib": [ "dom", "dom.iterable", "esnext" ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, "jsx": "react" /* Specify what JSX code is generated. */, "module": "es2015" /* Specify what module code is generated. */, "rootDir": "./src" /* Specify the root folder within your source files. */, "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, "types": [ "vitest/globals" ] /* Specify type package names to be included without being referenced in a source file. */, /* Emit */ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, /* Type Checking */ "strict": true /* Enable all strict type-checking options. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, "verbatimModuleSyntax": true /* Require `type` prefix for type-only imports */ }, "exclude": [ "./bin/**/*.test.ts", "./dist/**/*", "./bin/mocks/**/*", "./*.config.ts", "./src/testing-app/**", "./test-apps" ] } ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, environment: "happy-dom", }, });