# Frontend forms
Gadget provides helpers for quickly building great frontend form experiences.
## AutoForm autocomponents
Gadget will generate forms for you automatically that are pre-hooked up to call your app's API. Forms are customizable and use an underlying design system to ensure a consistent look and feel. This is the fastest way to get started with forms in Gadget.
The following will auto-generate a form for creating a new `post` and will include inputs for each field on the `post` model:
```tsx
import { AutoForm } from "@gadgetinc/react/auto/polaris";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.create);
return ;
};
```
See the [autocomponents guide](https://docs.gadget.dev/guides/frontend/autocomponents) for more info.
## `useActionForm` hook
The [`useActionForm`](https://docs.gadget.dev/reference/react#useActionForm) hook is the main hook for building forms with Gadget. It manages:
* retrieving initial form data
* getting and setting form values as users interact with a form
* submitting the form by calling an [action](https://docs.gadget.dev/guides/actions) in your app's backend
* displaying validation errors from the backend if they occur
`useActionForm` supports calling actions on [models](https://docs.gadget.dev/guides/models) as well as [global actions](https://docs.gadget.dev/guides/actions).
### Setting up a form
To set up a form, call the `useActionForm` helper with the action you want to run when the form is submitted. The `useActionForm` hook returns two main functions for form building:
* a `register` function that returns props for each form field
* and a `submit` function for calling to run the action
For example, if you have a backend model called `post` and an action called `create` on `post`, you can make a form for calling that action:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.create);
return (
);
};
```
This example form sets up two fields, one for the `title` field and one for the `content` field of the form.
### Retrieving initial form data
When the form is first rendered, `useActionForm`'s initial data can be:
* empty, like for `create` calls
* fetched from the backend using the `findBy` option
* seeded from an existing object or `GadgetRecord` object using the `defaultValues` option
#### Empty initial data
For forms that don't need any base data to start with, you can just call `useActionForm` with no option. Each form field will be initialized to an empty value.
For example, we can create a form for creating a new post that starts with empty data:
```tsx
// don't pass any options, and the form will start with empty data
const { register, submit } = useActionForm(api.post.create);
```
#### Fetching initial data from the backend
For forms that operate on existing records, `useActionForm` can fetch the record from the backend for you to seed the initial data for editing.
For example, we can create a form for editing an existing post by passing the `findBy` option to `useActionForm`:
```tsx
// pass the findBy option to fetch a post from the backend, and seed the initial form data with it
const { register, submit } = useActionForm(api.post.update, {
findBy: props.postId,
});
```
The `findBy` option can also fetch records by other fields that have a `findBy` function defined on your API client, which comes from that field having a Uniqueness field validation. This is useful for models with fields like a `slug` or another identifier that isn't an `id`. To find by a `findBy` field, pass an object with the field name as the key and the value to find by as the value:
```tsx
const { register, submit } = useActionForm(api.post.update, {
// find the initial post record by slug using api.post.findBySlug, and see the initial form values with that data
findBy: { slug: props.slug },
});
```
#### Passing explicit initial data
If you already have data in context for your form, or want to control exactly what data shows up in the initial values for each field, you can pass this data using the `defaultValues` option.
```tsx
// pass the defaultValues option to use hardcoded data as the initial data
const { register, submit } = useActionForm(api.finishTask, {
defaultValues: { id: props.user.id },
});
```
If you're calling an action on a model, and you have a `GadgetRecord` object returned by a record finder, you can pass the whole record to seed the initial values using the `defaultValues` option:
```tsx
import type { GadgetRecord } from "@gadget-client/example-app";
const PostForm = (props: { post: GadgetRecord }) => {
// pass the record option to use a GadgetRecord object as the initial data
const { register, submit } = useActionForm(api.post.update, {
defaultValues: props.post,
});
// ...
};
```
If you have values you want all users to start with in your form, you can set them into the `defaultValues` to seed the form with them:
```tsx
const { register, submit } = useActionForm(api.post.create, {
defaultValues: {
published: false,
author: { _link: currentUser.id },
categories: ["random thoughts"],
},
});
```
### Registering fields
The `register` function returned by `useActionForm` produces props for applying to each element that changes data in your form. ``, ``, ``, checkboxes, radio buttons, etc all can be registered with the `register` function.
Each registered input _must_ pass the identifier of the field it should set in the submission data. For example, if you're calling the `create` action for a model called `post`, and you want to set the `title` field, you would pass `post.title` to the `register` function:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.create);
return (
// ...
);
};
```
You can call the `register` function repeatedly for different form inputs to register multiple fields in the same form:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit } = useActionForm(api.post.create);
return (
<>
>
);
};
```
The `register` function comes from `react-hook-form`, the excellent form state management library that `useActionForm` uses under the hood. For more details on the `register` function, see the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/register).
#### Registering components without `ref` support
The top-level `register` function relies on the `ref` prop to correctly integrate with an input component. Base HTML elements like ``, ``, `` etc all support refs just fine, as well as many UI libraries. But, some libraries don't pass `ref`s down through their components to the low-level HTML elements.
For `ref`\-less components like these, you must use a `Controller` object to correctly register a field instead of the top-level `register` function.
To register a `ref`\-less component for a field, pass the field's name and the `control` object from `useActionForm` to a `Controller`. Then, mount your `ref`\-less component inside the `render` function of the controller, ensuring you pass the `field` props down to the component from the controller:
```tsx
import { useActionForm, Controller } from "@gadgetinc/react";
import { SomeUILibraryInput } from "some-ui-library-without-refs";
import { api } from "../api";
const PostForm = () => {
const { control, submit } = useActionForm(api.post.create);
return (
// ...
(
// pass the field props to the inner component (like `value` and `onChange`)
)}
/>
// ...
);
};
```
The `render` function of the controller will pass these props down to the input:
| prop | type | description |
| --- | --- | --- |
| `value` | `any` | sets the value of an input |
| `onChange` | `(event: React.ChangeEvent) => void` | detects changes the user makes |
| `onBlur` | `(event: React.ChangeEvent) => void` | for tracking dirty form state |
| `name` | `string` | for giving a unique name to the input in the DOM |
| `ref` | `React.MutableRef` | for allowing imperative focusing of the element |
As of writing, the following UI libraries are known to require the use of `Controller` because they have poor `ref` support or only use controlled components:
* `@shopify/polaris` (for components like ``)
* `@material-ui/core` (for components like ``)
* `react-select`
* `antd`
For more information on the `Controller` object, see the [`react-hook-form` docs](https://react-hook-form.com/docs/usecontroller/controller).
#### Registering `@shopify/polaris` components
`@shopify/polaris` form components require controlled component props like `value` and `onChange`, and don't support `ref`s well, so you must use a `` component to register its form inputs:
```tsx
import { Card, Form, FormLayout, TextField } from "@shopify/polaris";
import { useActionForm, Controller } from "@gadgetinc/react";
import { api } from "../api";
const AllowedTagForm = () => {
const { register, submit, control } = useActionForm(api.allowedTag.create);
return (
);
};
```
##### Registering controlled components from UI libraries
Controlled components from UI libraries often don't export a `ref`, or don't conform to the same style of props that a standard `` component might. For these components, you also need to use a `Controller` to register the field, instead of the top-level `register` function. See the above example for how to use `Controller`.
#### Registering number fields
By default, number fields in forms are treated as strings. If you want to submit a number field as a number, you can use the `valueAsNumber` option when registering your field on an input element:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
export default function () {
const { submit, register } = useActionForm(api.person.create);
// use valueAsNumber to submit the age as a number
return (
);
}
```
#### Registering date and datetime fields
If you have a date-only field on a model, it can be registered on an `` element with no special handling required:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
export default function () {
const { submit, register } = useActionForm(api.blog.create);
// date-only fields can be handled like any other field
return (
);
}
```
Datetime fields are trickier to handle. Using `` does not capture timezone information by default. You can always manually add a timezone offset to the value before submitting it, but this can be error-prone.
To handle datetime fields with timezone support, it can be easiest to use a library for the datepicker widget.
For example, you can use `react-datepicker`:
```tsx
import { useActionForm, Controller } from "@gadgetinc/react";
import { api } from "../api";
import { DatePicker } from "react-datepicker";
// import css here for example purposes, you can import in your main file
import "react-datepicker/dist/react-datepicker.css";
export default function () {
const { submit, control } = useActionForm(api.blog.create);
return (
);
}
```
or library components such as MUI's `@mui/x-date-pickers`:
```tsx
import { useActionForm, Controller } from "@gadgetinc/react";
import { api } from "../api";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
export default function () {
const { submit, control } = useActionForm(api.blog.create);
return (
);
}
```
#### Uploading files with forms
You can also include file uploads as part of a form submission. There are different ways to [store files](https://docs.gadget.dev/guides/models/storing-files) in Gadget, each with benefits and trade-offs.
##### Uploads using the File object in forms
The simplest way to handle file uploads is to use the `type="file"` attribute on an `` element. When a user selects a file, the file is stored in the form state as a `File` object. You can then submit the form with the file data as part of the form submission.
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
export default function () {
const { submit, register } = useActionForm(api.blog.create);
// use type="file" on the input element to allow users to select and submit a file
return (
);
}
```
##### Uploads using direct upload tokens
You can use direct upload tokens to upload larger files as part of form submission. You can use the `setValue` function returned by `useActionForm` to set the value of a file field in the form state, after uploading a file using the generated token.
This example implements drag-and-drop file upload:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
import { useState } from "react";
export default function () {
const { submit, setValue } = useActionForm(api.blog.create);
// state to manage the file upload
const [isUploading, setIsUploading] = useState(false);
const [fileName, setFileName] = useState(null);
return (
);
}
```
#### Registering fields on related models
You can use the [`useFieldArray` hook](https://docs.gadget.dev/reference/react#usefieldarray) along with `useActionForm` to register fields on related models. The `useFieldArray` hook comes from `react-hook-form` and allows you to build dynamic forms.
For example, a quiz application has a `quiz` model where each `quiz` record has many `questions`. You can register fields on the `quiz` and `question` models in the same form:
```tsx
import { useActionForm, useFieldArray } from "@gadgetinc/react";
import { api } from "../api";
export default function () {
// useActionForm for the parent `quiz` model
const { submit, register, control } = useActionForm(api.quiz.create);
// register fields on the 'questions' relationship of the 'quiz' model
const { fields, append, remove } = useFieldArray({ control, name: "quiz.questions" });
// use register on the quiz name field
// map over questions fields in array and register each existing question individually
return (
);
}
```
Some important things to note when using `useFieldArray`:
* The `name` option passed to `useFieldArray` should be the path to the related model from the parent. In this case, it's `quiz.questions`.
* The `fields` array returned by `useFieldArray` contains the current state of the field array. You can map over this array to render each field in the array.
* The `append` function returned by `useFieldArray` allows you to add new fields to the field array.
* The `remove` function returned by `useFieldArray` allows you to remove fields from the field array.
* The `type="button"` attribute is important for the append and remove buttons to prevent the form from submitting when clicked.
* The `field.id` value (`question.id` in the above example) must be used as the `key` prop on rendered list elements, NOT the `index`. This prevents re-renders from breaking the fields.
### Showing form state
Good UX practices dictate that our forms should give the user feedback as they make changes. If a field is in an error state, we need to show them which field, and what the error is. While the form is submitting, we should show them that it is happening, and report the outcome. All this state about the form is managed by `useActionForm` (and `react-hook-form` under the hood), and is available for you to use in your components.
To access the form's current state and errors, destructure the `formState` object returned from `useActionForm`, and use it to control your :
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const {
formState: { isDirty, isValid, errors, isSubmitting, isSubmitSuccessful },
} = useActionForm(api.post.create);
return (
// ...
);
};
```
The `formState` object includes the following properties:
| Name | Type | Description |
| --- | --- | --- |
| `isDirty` | `boolean` | `true` if the user has modified any inputs away from the `defaultValues`, and `false` otherwise |
| `dirtyFields` | `Record` | A map of fields to the dirty state for each field. Each field's property on the object `true` if the user has modified this field away from the default and `false` otherwise |
| `touchedFields` | `Record` | A map of fields to the touched state for each field. Each field's property on the object `true` if the user has modified this field at all and `false` otherwise |
| `defaultValues` | `Record` | The default values the form started out with, or has been `reset` to |
| `isSubmitted` | `boolean` | `true` if the form has ever been submitted, `false` otherwise |
| `isSubmitSuccessful` | `boolean` | `true` if the form has completed a submission that encountered no errors, `false` otherwise |
| `isSubmitting` | `boolean` | `true` if the form is currently submitting to the backend, `false` otherwise |
| `isLoading` | `boolean` | `true` if the form is currently loading data from the backend to populate the initial values or another input, `false` otherwise |
| `submitCount` | `number` | Count of times the form has been submitted |
| `isValid` | `boolean` | `true` if the form has no validation errors currently, `false` otherwise |
| `isValidating` | `boolean` | `true` if the form is currently validating data, `false` otherwise |
| `errors` | `Record` | A map of any validation errors currently present on each field |
For more on the `formState` object, see the [`react-hook-form` docs](https://react-hook-form.com/api/useform/formstate).
### Submitting the form
Once you have a form setup with inputs for users, you can call the action with the changed form data by calling the `submit` function returned from `useActionForm`:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit, formState } = useActionForm(api.post.create);
return ;
};
```
When called, the `submit` function will assemble the right variables for your Gadget backend and call the action. If the action execution succeeds, `isSubmitSuccessful` will become true and `submit` will resolve with the result of calling the action. You can await the submission, and show feedback to the user depending on the outcome:
```tsx
import toaster from "some-toast-library";
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit, formState } = useActionForm(api.post.create);
return (
);
};
```
`submit` is an async function that will return the result of calling the backend Gadget action as a `{data, error, fetching}` triple. It's the same value as is returned by the [`useAction` hook](https://docs.gadget.dev/reference/react#useaction). If the submission encounters an error, the returned value will look like:
```markdown
{
data: null,
error: Error
}
```
If the submission succeeded, the returned value will look like:
```markdown
{
data: Result, // a GadgetRecord for model actions, the action return value for Global Actions
error: null
};
```
#### Submitting the form and excluding fields
There are scenarios where you'd like to exclude some fields from form submission when calling an action. You've got two options and they depend on your use case:
##### Setting fields as `ReadOnly` with the `select` option
When using an `update` action, we recommend using the `select` option and setting fields as `ReadOnly` to exclude them from the form submission. These fields are still fetched from the backend when using `findBy`.
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit, formState } = useActionForm(api.post.update, {
findBy: "1",
select: {
title: true,
body: "ReadOnly",
},
});
return ;
};
```
##### Using the `send` option
When using an action that doesn't utilize `select` you can use the `send` option to control which fields are submitted.
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { register, submit, formState } = useActionForm(api.post.create, {
findBy: "1",
send: ["title", "body"],
});
return ;
};
```
### Showing errors
The `errors` object returned in the `formState` object reflects validation errors from invalid user inputs to your form. You can add validations as you register fields, and you can explicitly mark inputs as invalid with the `setError` function.
For example, we can add a client-side `required` validation to the title element, and show a message when the title is blank:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const {
formState: { errors },
} = useActionForm(api.post.create);
return (
<>
{/** Render an error message if there is an error on the title attribute */}
{errors.title?.type === "required" &&
Title is required
}
>
);
};
```
#### Handling server-side validations
Server-side validations on models are automatically handled by `useActionForm`. When the input is invalid, your app's GraphQL API will return structured data that describes each of the invalid fields, and `useActionForm` will automatically update the `errors` object with the right messages.
##### Adding client-side validations
Client-side validations give a nice experience for your user where they get real-time feedback on their entries into a form, without needing to wait until they submit the form to find out if what they've entered is valid.
`useActionForm` supports client-side validations via `react-hook-form`'s base validation support, as well as its schema validator support.
For basic validations, you can pass validation information when you `register` a field (with either `register` or a `Controller/>`).
```tsx
export const Form = () => {
const { register } = useActionForm(api.post.create);
return ;
};
```
Then, the `formState.errors` object will contain an error on the `title` attribute if the field is empty.
The available base validation rules are:
* `required` for requiring some input into a field
* `min` for a minimum number
* `max` for a maximum number
* `minLength` for a minimum length string
* `maxLength` for a maximum length string
* `pattern` for validating against a regexp
* `validate` for passing a custom validation function
Read more about `react-hook-form`'s built-in base validations in the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/register).
#### Setting errors explicitly
`useActionForm` returns a `setError` function for explicitly setting errors on a field imperatively.
```tsx
const { setError } = useActionForm(api.post.create);
useEffect(() => {
setError("title", {
type: "manual",
message: "This is an error on the title field",
});
}, [setError]);
```
Read more about `react-hook-form`'s `setError` function in the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/seterror).
`useActionForm` also returns a `clearErrors` function for removing all or some errors on the form imperatively.
```tsx
const { clearErrors } = useActionForm(api.post.create);
// ...
;
```
Read more about `react-hook-form`'s `clearErrors` function in the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/clearerrors).
#### Handling transport errors
In addition to validation errors, your application can encounter other errors when handling form submissions like the user's internet being disconnected or a 500 error during processing. These errors are often unexpected, and they don't pertain to any field in particular.
To display these errors to the user, destructure the top-level `error` object from calling `useActionForm`:
```tsx
import { useActionForm } from "@gadgetinc/react";
import { api } from "../api";
const PostForm = () => {
const { error } = useActionForm(api.post.create);
return (
<>
{error &&
There was an error: {error.message}
}
{/** ... */}
>
);
};
```
The `error` object will be `null` if no errors have been encountered, or contain any thrown `Error` objects. It will be populated with any transport errors fetching the initial data for the form (when using `findBy`), as well as any transport errors when submitting the form. The `error` object won't be populated if server-side validation errors are encountered.
### Resetting the form
After a form is submitted, it's often useful to reset the form to a clean state. `useActionForm` returns a `reset` function for this purpose. You can call `reset` to reset the form to its initial state, or pass an object to `reset` to reset the form to a specific set of values.
```tsx
const { reset } = useActionForm(api.post.create);
// call reset to reset the form's value
reset();
// call reset with specific values to reset to those values
reset({ title: "new form title", body: "new body" });
```
`reset` also accepts specific options to keep various elements of the form state in place, like `keepErrors`, `keepDirty`, etc. See the [`react-hook-form` docs](https://react-hook-form.com/docs/useform/reset) for more info on the `reset` function.
#### Resetting automatically after submitting
`useActionForm` will _not_ reset the data automatically after a successful submission. If you'd like to reset your form's data after a successful submission, you can use a `useEffect` to do so in your form component:
```tsx
const {
reset,
formState: { isSubmitSuccessful },
} = useActionForm(api.post.create);
useEffect(() => {
reset({
data: "new data",
});
}, [isSubmitSuccessful]);
```
### Form file uploads
`useActionForm` does not currently support file uploads.
To handle file uploads as part of a form, we recommend checking out our storing files guide where you can learn how to build [frontend upload forms](https://docs.gadget.dev/guides/models/storing-files#writing-frontend-upload-forms). You can alternatively use other popular React libraries for building forms that support file uploads. For more information, check out [other form approaches](https://docs.gadget.dev/guides/frontend/forms#other-form-approaches).
## Other form approaches
`useActionForm` is the recommended way to build forms with Gadget, but it isn't the only way.
### `useAction` hook
If you prefer to manage your form state on your own but still want to use your app's GraphQL API for processing the submitted data, you can use the `useAction` hook for calling actions. The `useAction` hook takes care of managing the request lifecycle, error handling, and ensuring React re-renders appropriately. It serves as an excellent method for invoking actions within your app.
For example, if you have a `post` model, you can create a new post by calling the `act` function returned by the `useAction` hook:
```tsx
const [{ data, fetching, error }, act] = useAction(api.post.create);
// when ready, run the `act` function to trigger the action
act({ title: "Example Post", body: "some post content" });
```
We can use the `useAction` hook in a React component that calls the action with data from a form:
```tsx
import { useState } from "react";
import { useAction } from "@gadgetinc/react";
import { api } from "../api";
export const CreatePostForm = (props) => {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [{ data, fetching, error }, act] = useAction(api.post.create);
if (fetching) {
return
Saving...
;
}
if (error) {
return
Error: {error.message}
;
}
return (
);
};
```
#### No-input forms
Certain forms do not incorporate input elements; instead, they trigger backend logic when users click on them. For instance, a button that triggers a data sync may not require any user input but still needs to trigger an action. In cases where there is no need to manage form state, opting for `useAction` over `useActionForm` is often a more straightforward approach to handle these elements.
For example, if we have a `post` model that has a `publish` action, we can build a button to trigger this mutation with the `useAction` hook:
```tsx
import { useAction } from "@gadgetinc/react";
import { api } from "../api";
export const PostPublishButton = (props: { post: { id: string } }) => {
const [{ data, fetching, error }, act] = useAction(api.post.publish);
return (
);
};
```
### Other popular libraries
The React ecosystem has several other approaches to form building which can be used with Gadget. Popular libraries like [`formik`](https://formik.org/) or plain state management with `useState` and `useReducer` in React can be used to build forms with Gadget as well.