Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/forge42dev/remix-hook-form
Open source wrapper for react-hook-form aimed at Remix.run
https://github.com/forge42dev/remix-hook-form
forms hooks javascript react react-hooks remix remix-run
Last synced: 5 days ago
JSON representation
Open source wrapper for react-hook-form aimed at Remix.run
- Host: GitHub
- URL: https://github.com/forge42dev/remix-hook-form
- Owner: forge42dev
- License: mit
- Created: 2023-04-10T13:35:05.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-10-15T10:59:57.000Z (29 days ago)
- Last Synced: 2024-10-15T12:55:09.471Z (29 days ago)
- Topics: forms, hooks, javascript, react, react-hooks, remix, remix-run
- Language: TypeScript
- Homepage:
- Size: 1.02 MB
- Stars: 344
- Watchers: 8
- Forks: 29
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.MD
Awesome Lists containing this project
README
# remix-hook-form
![GitHub Repo stars](https://img.shields.io/github/stars/Code-Forge-Net/remix-hook-form?style=social)
![npm](https://img.shields.io/npm/v/remix-hook-form?style=plastic)
![GitHub](https://img.shields.io/github/license/Code-Forge-Net/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/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.
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!
## Install
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
```ts
import { useRemixForm, getValidatedFormData } from "remix-hook-form";
import { Form } from "@remix-run/react";
import { zodResolver } from "@hookform/resolvers/zod";
import * as zod from "zod";
import { ActionFunctionArgs, json } from "@remix-run/node"; // or cloudflare/denoconst 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 }: ActionFunctionArgs) => {
const { errors, data, receivedValues: defaultValues } =
await getValidatedFormData(request, resolver);
if (errors) {
// The keys "errors" and "defaultValues" are picked up automatically by useRemixForm
return json({ errors, defaultValues });
}// Do something with the data
return json(data);
};export default function MyForm() {
const {
handleSubmit,
formState: { errors },
register,
} = useRemixForm({
mode: "onSubmit",
resolver,
});return (
Name:
{errors.name &&{errors.name.message}
}
Email:
{errors.email &&{errors.email.message}
}
Submit
);
}
```## 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 }: ActionFunctionArgs) => {
const { errors, data } = await getValidatedFormData(
request,
resolver,
true,
);
if (errors) {
return json({ errors });
}
// Do something with the data
};
```## File Upload example
For more details see [File Uploads guide](https://remix.run/docs/en/main/guides/file-uploads) in Remix docs.
```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" });
};export const action = async ({ request }: ActionFunctionArgs) => {
// use the upload handler to parse the file
const formData = await unstable_parseMultipartFormData(
request,
fileUploadHandler(),
);
// 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({ result: "success" });
};
```## 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:https://youtu.be/iom5nnj29sY?si=l52WRE2bqpkS2QUh
## 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 }: ActionFunctionArgs) => {
// Takes the request from the frontend, parses and validates it and returns the data
const { errors, data } =
await getValidatedFormData(request, resolver);
if (errors) {
return json({ 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 }: ActionFunctionArgs) => {
// 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 json({ 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 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`.
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 }: ActionFunctionArgs) => {
// 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 json({ 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 }: ActionFunctionArgs) => {
// 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 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.
- `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. 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.**`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.
**`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"
},
});```
### 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
);
}```
### useRemixFormContext
Exactly the same as [`useFormContext`](https://react-hook-form.com/api/useformcontext/) 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
);
}const NestedInput = () => {
const { register } = useRemixFormContext(); // retrieve all hook methods
return ;
}```
## Support
If you like the project, please consider supporting us by giving a ⭐️ on Github.
## LicenseMIT
## Bugs
If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/Code-Forge-Net/remix-hook-form/issues)
## Contributing
Thank you for considering contributing to Remix-hook-form! We welcome any contributions, big or small, including bug reports, feature requests, documentation improvements, or code changes.
To get started, please fork this repository and make your changes in a new branch. Once you're ready to submit your changes, please open a pull request with a clear description of your changes and any related issues or pull requests.
Please note that all contributions are subject to our [Code of Conduct](https://github.com/Code-Forge-Net/remix-hook-form/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code.
We appreciate your time and effort in contributing to Remix-hook-form and helping to make it a better tool for the community!