Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: forge-42/remix-hook-form
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.0.4
Choose a base ref
...
head repository: forge-42/remix-hook-form
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Sep 10, 2024

  1. Copy the full SHA
    dd53809 View commit details
  2. Copy the full SHA
    3c6f481 View commit details
  3. Copy the full SHA
    823fc9d View commit details

Commits on Sep 17, 2024

  1. Merge pull request #110 from vmosyaykin/fix-prettier

    Chore: fix prettier npm scripts
    AlemTuzlak authored Sep 17, 2024
    Copy the full SHA
    97728f0 View commit details
  2. Merge pull request #109 from vmosyaykin/default-values

    Improve defaultValue prop resolution
    AlemTuzlak authored Sep 17, 2024
    Copy the full SHA
    6d1296b View commit details
  3. fix for undefined handling

    AlemTuzlak committed Sep 17, 2024
    Copy the full SHA
    883645f View commit details
  4. Copy the full SHA
    5eae55a View commit details
  5. update

    AlemTuzlak committed Sep 17, 2024
    Copy the full SHA
    61951f0 View commit details
  6. Merge pull request #112 from forge42dev/undefined-handling

    fix for undefined handling
    AlemTuzlak authored Sep 17, 2024
    Copy the full SHA
    30e0787 View commit details
  7. readme update

    AlemTuzlak committed Sep 17, 2024
    Copy the full SHA
    634cd7c View commit details
  8. Copy the full SHA
    72fcb24 View commit details

Commits on Oct 5, 2024

  1. added workflows

    AlemTuzlak committed Oct 5, 2024
    Copy the full SHA
    be02359 View commit details

Commits on Oct 11, 2024

  1. Copy the full SHA
    3cf99ad View commit details

Commits on Oct 15, 2024

  1. Copy the full SHA
    5b13c0f View commit details
  2. Copy the full SHA
    0fcbccc View commit details
  3. Merge pull request #115 from aon/feat/add-action-config-from-form

    feat: add action support from Form config
    AlemTuzlak authored Oct 15, 2024
    Copy the full SHA
    a2629af View commit details

Commits on Nov 23, 2024

  1. React router v7 support

    AlemTuzlak committed Nov 23, 2024
    Copy the full SHA
    7035cf8 View commit details

Commits on Dec 12, 2024

  1. Copy the full SHA
    7d87705 View commit details
  2. Merge pull request #138 from forge42dev/fix_for_get_form_data

    Fix for getValidatedFormData
    AlemTuzlak authored Dec 12, 2024
    Copy the full SHA
    b3f23f9 View commit details
  3. fix for fiellist

    AlemTuzlak committed Dec 12, 2024
    Copy the full SHA
    d06e931 View commit details

Commits on Dec 23, 2024

  1. Copy the full SHA
    f3c93aa View commit details

Commits on Jan 5, 2025

  1. react 19 support

    AlemTuzlak committed Jan 5, 2025
    Copy the full SHA
    a8a9b93 View commit details
  2. Merge pull request #146 from forge42dev/react-19

    react 19 support
    AlemTuzlak authored Jan 5, 2025
    Copy the full SHA
    bc65d42 View commit details
  3. Merge pull request #144 from david-szabo97/fix/submit-action-basename

    Remove basename from submit target action
    AlemTuzlak authored Jan 5, 2025
    Copy the full SHA
    a7619e5 View commit details
  4. version bump

    AlemTuzlak committed Jan 5, 2025
    Copy the full SHA
    7bbaef9 View commit details

Commits on Jan 17, 2025

  1. Copy the full SHA
    e4fa40d View commit details
  2. Copy the full SHA
    9e20d16 View commit details
  3. Copy the full SHA
    b2de7e7 View commit details

Commits on Jan 18, 2025

  1. adding Blob to tryParseJson

    videvide committed Jan 18, 2025
    Copy the full SHA
    e6eccc4 View commit details
  2. update comment

    videvide committed Jan 18, 2025
    Copy the full SHA
    614569a View commit details
  3. Copy the full SHA
    3ee612c View commit details

Commits on Jan 21, 2025

  1. Merge pull request #150 from videvide/fix-file-uploads

    Fix: file uploads
    AlemTuzlak authored Jan 21, 2025
    Copy the full SHA
    71f7ed5 View commit details
  2. Copy the full SHA
    3d60844 View commit details

Commits on Jan 28, 2025

  1. Copy the full SHA
    68663fb View commit details
  2. Merge pull request #153 from melanieseltzer/melanieseltzer-patch-1

    Docs improvement to clarify when submitData hook option is ignored
    AlemTuzlak authored Jan 28, 2025
    Copy the full SHA
    51cb711 View commit details
Showing with 8,162 additions and 14,264 deletions.
  1. +24 −0 .github/publish-commit.yaml
  2. +19 −0 .github/publish.yaml
  3. +3 −0 .gitignore
  4. +151 −43 README.md
  5. +6,949 −13,454 package-lock.json
  6. +25 −30 package.json
  7. +136 −8 src/hook/index.test.tsx
  8. +61 −28 src/hook/index.tsx
  9. +0 −4 src/testing-app/.eslintrc.js
  10. +0 −6 src/testing-app/.gitignore
  11. +0 −58 src/testing-app/README.md
  12. +0 −18 src/testing-app/app/entry.client.tsx
  13. +0 −135 src/testing-app/app/entry.server.tsx
  14. +0 −36 src/testing-app/app/root.tsx
  15. +0 −122 src/testing-app/app/routes/_index.tsx
  16. +0 −87 src/testing-app/app/zod.ts
  17. +0 −34 src/testing-app/package.json
  18. BIN src/testing-app/public/favicon.ico
  19. +0 −11 src/testing-app/remix.config.js
  20. +0 −2 src/testing-app/remix.env.d.ts
  21. +0 −22 src/testing-app/tsconfig.json
  22. +105 −3 src/utilities/index.test.ts
  23. +73 −43 src/utilities/index.ts
  24. +4 −0 test-apps/react-router/.dockerignore
  25. +7 −0 test-apps/react-router/.gitignore
  26. +22 −0 test-apps/react-router/Dockerfile
  27. +25 −0 test-apps/react-router/Dockerfile.bun
  28. +26 −0 test-apps/react-router/Dockerfile.pnpm
  29. +98 −0 test-apps/react-router/README.md
  30. +12 −0 test-apps/react-router/app/app.css
  31. +75 −0 test-apps/react-router/app/root.tsx
  32. +3 −0 test-apps/react-router/app/routes.ts
  33. +103 −0 test-apps/react-router/app/routes/home.tsx
  34. +23 −0 test-apps/react-router/app/welcome/logo-dark.svg
  35. +23 −0 test-apps/react-router/app/welcome/logo-light.svg
  36. +89 −0 test-apps/react-router/app/welcome/welcome.tsx
  37. +33 −0 test-apps/react-router/package.json
  38. BIN test-apps/react-router/public/favicon.ico
  39. +3 −0 test-apps/react-router/react-router.config.ts
  40. +22 −0 test-apps/react-router/tailwind.config.ts
  41. +28 −0 test-apps/react-router/tsconfig.json
  42. +15 −0 test-apps/react-router/vite.config.ts
  43. +5 −71 tsconfig.json
  44. +0 −49 vite.config.ts
24 changes: 24 additions & 0 deletions .github/publish-commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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
19 changes: 19 additions & 0 deletions .github/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -104,3 +104,6 @@ dist
.tern-port

/build
.history
.react-router
.turbo
194 changes: 151 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -7,22 +7,28 @@
![npm](https://img.shields.io/npm/dw/remix-hook-form?style=plastic)
![GitHub top language](https://img.shields.io/github/languages/top/Code-Forge-Net/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 [Remix](https://remix.run) 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.
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

npm install remix-hook-form react-hook-form
```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:

npm install zod @hookform/resolvers
```bash
npm install zod @hookform/resolvers
```

```ts
import { useRemixForm, getValidatedFormData } from "remix-hook-form";
@@ -80,59 +86,147 @@ export default function MyForm() {
}
```

## File Upload example
## Serialization of values client => server

For more details see [File Uploads guide](https://remix.run/docs/en/main/guides/file-uploads) in Remix docs.
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
import { unstable_parseMultipartFormData, ActionFunctionArgs, json } from "@remix-run/node"; // or cloudflare/deno

// You can implement your own uploadHandler, this one serves as a basic example of how to handle file uploads
export const fileUploadHandler =
(): UploadHandler =>
async ({ data, filename }) => {
const chunks = [];
for await (const chunk of data) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
// If there's no filename, it's a text field and we can return the value directly
if (!filename) {
const textDecoder = new TextDecoder();
return textDecoder.decode(buffer);
}
// Otherwise, it's a file and we need to return a File object
return new File([buffer], filename, { type: "image/jpeg" });
};
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<typeof formDataZodSchema>;

const resolver = zodResolver(formDataZodSchema);

export const action = async ({ request }: ActionFunctionArgs) => {
// use the upload handler to parse the file
const formData = await unstable_parseMultipartFormData(
const { errors, data } = await getValidatedFormData<SchemaFormData>(
request,
fileUploadHandler(),
resolver,
true,
);
// The file will be there
console.log(formData.get("file"));
// validate the form data
const { errors, data } = await validateFormData(formData, resolver);
if (errors) {
return json({ errors }, {
status: 422,
});
return json({ errors });
}
return json({ result: "success" });
// 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 Remix 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 Remix. It also covers how to use this package. You can find it here:
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

@@ -186,7 +280,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {

### validateFormData

validateFormData is a utility function that can be used to validate form data in your action. It takes two arguments: the request 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`.
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.

@@ -260,19 +354,22 @@ If you're using a GET request formData is not available on the request so you ca

### useRemixForm

`useRemixForm` is a hook that can be used to create a form in your 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:
`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.
- `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 Remix, such as `{ replace: true }` to replace the current history entry instead of pushing a new one. The `submitConfig` trumps `Form` props from Remix. But the `Form` props are used if no submitConfig is provided.
- `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 Remix's `useSubmit` and `useActionData` hooks.
- `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 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.
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`**

@@ -327,6 +424,17 @@ The `errors` object inside `formState` is automatically populated with the error
},
});

// 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
Loading