@shopify/react-form
Version:
Manage React forms tersely and safely-typed with no magic using React hooks
1,263 lines (1,027 loc) • 36.9 kB
Markdown
# `@shopify/react-form`
[](https://travis-ci.org/Shopify/quilt)
[](LICENSE.md) [](https://badge.fury.io/js/%40shopify%2Freact-form.svg) [](https://img.shields.io/bundlephobia/minzip/@shopify/react-form.svg)
Manage React forms tersely and safely-typed with no magic using React hooks. Build up your form logic by combining hooks yourself, or take advantage of the smart defaults provided by the powerful `useForm` hook.
## Table of contents
1. [Installation](#installation)
1. [Usage](#usage)
1. [Quickstart](#quickstart)
1. [Composition](#composition)
1. [API](#api)
1. [Hooks](#hooks)
1. [useField](#usefield)
1. [useChoiceField](#usechoicefield)
1. [useList](#uselist)
1. [useDynamicList](#usedynamiclist)
1. [useForm](#useform)
1. [useDirty](#usedirty)
1. [useReset](#usereset)
1. [useSubmit](#usesubmit)
1. [Validation](#validation)
1. [inline](#validation)
1. [multiple](#validation)
1. [dependencies](#validation)
1. [built-in](#validation)
1. [validator](#validation)
1. [Utilities](#utilities)
1. [reduceFields](#utilities)
1. [getValues](#utilities)
1. [fieldsToArray](#utilities)
1. [makeCleanFields](#utilities)
1. [FAQ](#faq)
## Installation
```bash
$ yarn add @shopify/react-form
```
## Usage
### Quickstart
This package exports a variety of hooks for all things form state, but the quickest way to get up and running is with the hooks `useForm` and `useField`.
```tsx
import {useForm, useField} from '@shopify/react-form';
```
By passing `useForm` a dictionary of field objects generated by `useField` calls, you can build out a fully featured form with minimal effort.
```tsx
import React from 'react';
import {useField, useForm} from '@shopify/react-form';
function MyComponent() {
const {
fields: {title},
submit,
submitting,
dirty,
reset,
submitErrors,
makeClean,
} = useForm({
fields: {
title: useField('some default title'),
},
onSubmit: async fieldValues => {
return {status: 'fail', errors: [{message: 'bad form data'}]};
},
});
const loading = submitting ? <p className="loading">loading...</p> : null;
const errors =
submitErrors.length > 0 ? (
<p className="error">{submitErrors.join(', ')}</p>
) : null;
return (
<form onSubmit={submit}>
{loading}
{errors}
<div>
<label htmlFor="title">
Title
<input
id="title"
name="title"
value={title.value}
onChange={title.onChange}
onBlur={title.onBlur}
/>
</label>
{title.error && <p className="error">{title.error}</p>}
</div>
<button type="button" disabled={!dirty} onClick={reset}>
Reset
</button>
<button type="submit" disabled={!dirty} onClick={submit}>
Submit
</button>
</form>
);
}
```
The hooks provided here also work swimmingly with [`@shopify/polaris`](https://polaris.shopify.com/).
```tsx
import React from 'react';
import {useField, useForm, notEmpty, lengthMoreThan} from '@shopify/react-form';
import {
Page,
Layout,
FormLayout,
Form,
Card,
TextField,
ContextualSaveBar,
Frame,
Banner,
} from '@shopify/polaris';
export default function MyComponent() {
const {
fields,
submit,
submitting,
dirty,
reset,
submitErrors,
makeClean,
} = useForm({
fields: {
title: useField({
value: '',
validates: [
notEmpty('Title is required'),
lengthMoreThan(3, 'Title must be more than 3 characters'),
],
}),
description: useField(''),
},
async onSubmit(form) {
const remoteErrors = []; // your API call goes here
if (remoteErrors.length > 0) {
return {status: 'fail', errors: remoteErrors};
}
return {status: 'success'};
},
});
const contextBar = dirty ? (
<ContextualSaveBar
message="Unsaved product"
saveAction={{
onAction: submit,
loading: submitting,
disabled: false,
}}
discardAction={{
onAction: reset,
}}
/>
) : null;
const errorBanner =
submitErrors.length > 0 ? (
<Layout.Section>
<Banner status="critical">
<p>There were some issues with your form submission:</p>
<ul>
{submitErrors.map(({message}, index) => {
return <li key={`${message}${index}`}>{message}</li>;
})}
</ul>
</Banner>
</Layout.Section>
) : null;
return (
<Frame>
<Form onSubmit={submit}>
<Page title="New Product">
{contextBar}
<Layout>
{errorBanner}
<Layout.Section>
<Card sectioned>
<FormLayout>
<TextField label="Title" {...fields.title} />
<TextField
multiline
label="Description"
{...fields.description}
/>
</FormLayout>
</Card>
</Layout.Section>
</Layout>
</Page>
</Form>
</Frame>
);
}
```
### Composition
`useForm` gives us a lot in a small package, but one of the main benefits of hooks is composition. If we prefer, we can build our pages with much more granular use of hooks. This is especially valuable if you only need some of the behaviours of `useForm`, or need some aspect of the state to be managed differently.
```tsx
import React from 'react';
import {
useField,
useReset,
useDirty,
useSubmit,
notEmpty,
lengthMoreThan,
} from '@shopify/react-form';
import {
Page,
Layout,
FormLayout,
Form,
Card,
TextField,
ContextualSaveBar,
Frame,
Banner,
} from '@shopify/polaris';
export default function MyComponent() {
const title = useField({
value: '',
validates: [
notEmpty('Title is required'),
lengthMoreThan(3, 'Title must be more than 3 characters'),
],
});
const description = useField('');
const fields = {title, description};
// track whether any field has been changed from its initial values
const dirty = useDirty(fields);
// generate a reset callback
const reset = useReset(fields);
// handle submission state
const {submit, submitting, errors, setErrors} = useSubmit(
async fieldValues => {
const remoteErrors = []; // your API call goes here
if (remoteErrors.length > 0) {
return {status: 'fail', errors: remoteErrors};
}
return {status: 'success'};
},
fields,
);
const contextBar = dirty && (
<ContextualSaveBar
message="Unsaved product"
saveAction={{
onAction: submit,
loading: submitting,
disabled: false,
}}
discardAction={{
onAction: reset,
}}
/>
);
const errorBanner = errors.length > 0 && (
<Layout.Section>
<Banner status="critical">
<p>There were some issues with your form submission:</p>
<ul>
{errors.map(({message}, index) => {
return <li key={`${message}${index}`}>{message}</li>;
})}
</ul>
</Banner>
</Layout.Section>
);
return (
<Frame>
<Form onSubmit={submit}>
<Page title="New Product">
{contextBar}
<Layout>
{errorBanner}
<Layout.Section>
<Card sectioned>
<FormLayout>
<TextField label="Title" {...fields.title} />
<TextField
multiline
label="Description"
{...fields.description}
/>
</FormLayout>
</Card>
</Layout.Section>
</Layout>
</Page>
</Form>
</Frame>
);
}
```
## API
This section details the individual functions exported by `@shopify-react-form`. For more detailed typing information see the `.d.ts` files.
### Hooks
#### `useField()`
A custom hook for handling the state and validations of an input field.
##### Signature
```tsx
const field = useField(config, validationDependencies);
```
###### Parameters:
- `config`, The default value of the input, or a configuration object specifying both the value and validation config.
- `validationDependencies`, An optional array of values for determining when to regenerate the field's validation callbacks. Any value that is referenced by a validator other than those passed into it should be included.
###### Return value:
A `Field` object representing the state of your input. It also includes functions to manipulate that state. Generally, you will want to pass these callbacks down to the component or components representing your input.
##### Examples
In its simplest form `useField` can be called with a single parameter for the default value of the field.
```tsx
const field = useField('default value');
```
You can also pass a more complex configuration object specifying a validation function.
```tsx
const field = useField({
value: someRemoteData.title,
validates: title => {
if (title.length > 3) {
return 'Title must be longer than three characters';
}
},
});
```
You may also pass multiple validators.
```tsx
const field = useField({
value: someRemoteData.title,
validates: [
title => {
if (title.length > 3) {
return 'Title must be longer than three characters';
}
},
title => {
if (!title.includes('radical')) {
return 'Only radical items are allowed';
}
},
],
});
```
Generally, you will want to use the object returned from `useField` to handle state for exactly one form input.
```tsx
const field = useField('default value');
const fieldError = field.error ? <p>{field.error}</p> : null;
return (
<div>
<label htmlFor="test-field">
Test field{' '}
<input
id="test-field"
name="test-field"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
/>
</label>
{fieldError}
</div>
);
```
If using `@shopify/polaris` or other custom components that support `onChange`, `onBlur`, `value`, and `error` props then
you can accomplish the above more tersely by using the ES6 spread `...` operator.
```tsx
const title = useField('default title');
return <TextField label="Title" {...title} />;
```
##### Remarks
**Reinitialization:** If the `value` property of the field configuration changes between calls to `useField`, the field will be reset to use it as its new default value.
**Imperative methods:** The returned `Field` object contains a number of methods used to imperatively alter its state. These should only be used as escape hatches where the existing hooks and components do not make your life easy, or to build new abstractions in the same vein as `useForm`, `useSubmit` and friends.
#### `useChoiceField()`
An extension to `useField()` that produces a new field compatible with `<Checkbox />` and `<RadioButton />` from `@shopify/polaris`. Note that this hook cannot be used within a `useForm`, `useChoiceField` can only be used for standalone fields.
##### Signature
The signature is identical to `useField()` for `boolean` fields.
```tsx
const simple = useChoiceField(false);
const complex = useChoiceField(config, validationDependencies);
```
##### Examples
Fields produced by `useChoiceField` operate just like normal fields, except they have been converted by `asChoiceField` automatically which swaps the `value` member for `checked` to provide compatibility with `Checkbox` and `RadioButton`.
```tsx
const enabled = useChoiceField(false);
return <Checkbox label="Enabled" {...enabled} />;
```
For fields that need to be compatible with choice components on the fly, the `asChoiceField` utility function can be used instead to adapt the field for a specific composition.
```tsx
const enabled = useField(false);
return <Checkbox label="Enabled" {...asChoiceField(enabled)} />;
```
#### `asChoiceField()`
A utility to convert a `Field<Value>` into a derivative that is compatible with `<Checkbox />` and `<RadioButton />` from `@shopify/polaris`. The `value` member will be replaced by a new `checked` member, and the `onChange` will be replaced with a choice component compatible callback.
##### Signature
<!-- The signature is identical to `useField()` for `boolean` fields. -->
`asChoiceField` consumes an existing `Field<Value>`, and optionally a `checkedValue` predicate when dealing with multi-value base fields.
```tsx
const simpleField = useField(false);
const simple = asChoiceField(simpleField);
const multiField = useField<'A' | 'B'>('A');
const multiA = asChoiceField(multiField, 'A');
const multiB = asChoiceField(multiField, 'B');
```
##### Examples
`asChoiceField` is used as a helper to expand an existing field into a choice component (`<Checkbox />` and `<RadioButton />`).
```tsx
const enabled = useChoiceField(false);
return <Checkbox label="Enabled" {...enabled} />;
```
You can also expand an existing field directly into a choice component by wrapping the field in `asChoiceField` if you want to retain the original field's shape.
```tsx
const enabled = useField(false);
return <Checkbox label="Enabled" {...asChoiceField(enabled)} />;
```
For multi-value base fields, we expand the same field into multiple `<RadioButton>` components.
```tsx
const selectedOption = useField<'A' | 'B'>('A');
return (
<Stack vertical>
<RadioButton label="A" {...asChoiceField(selectedOption, 'A')} />
<RadioButton label="B" {...asChoiceField(selectedOption, 'B')} />
<RadioButton label="C" {...asChoiceField(selectedOption, 'C')} />
</Stack>
);
```
#### `useList()`
A custom hook for handling the state and validations of fields for a list of objects.
##### Signature
```tsx
const fields = useList(config, validationDependencies);
```
###### Parameters:
- `config`, A configuration object specifying both the value and validation config.
- `validationDependencies`, An array of dependencies to use to decide when to regenerate validators.
###### Return value:
A list of dictionaries of `Field` objects representing the state of your input. It also includes functions to manipulate that state. Generally, you will want to pass these callbacks down to the component or components representing yournput.
##### Examples
In its simplest form `useList` can be called with a single parameter with the list to derive default values and structure from.
```tsx
const field = useList([
{title: '', description: ''},
{title: '', description: ''},
]);
```
You can also pass a more complex configuration object specifying a validation dictionary.
```tsx
const field = useList({
list: [
{title: '', description: ''},
{title: '', description: ''},
],
validates: {
title: title => {
if (title.length > 3) {
return 'Title must be longer than three characters';
}
},
description: description => {
if (description === '') {
return 'Description is required!';
}
},
},
});
```
Generally, you will want to use the list returned from `useList` by looping over it in your JSX.
```tsx
function MyComponent() {
const variants = useList([
{title: '', description: ''},
{title: '', description: ''},
]);
return (
<ul>
{variants.map((fields, index) => (
<li key={index}>
<label htmlFor={`title-${index}`}>
title{' '}
<input
id={`title-${index}`}
name={`title-${index}`}
value={fields.title.value}
onChange={fields.title.onChange}
onBlur={fields.title.onBlur}
/>
</label>
{field.title.error && <p>{field.title.error}</p>}
<label htmlFor={`description-${index}`}>
description{' '}
<input
id={`description-${index}`}
name={`description-${index}`}
value={fields.description.value}
onChange={fields.description.onChange}
onBlur={fields.description.onBlur}
/>
</label>
{field.description.error && <p>{field.description.error}</p>}
</li>
))}
</ul>
);
}
```
If using `@shopify/polaris` or other custom components that support `onChange`, `onBlur`, `value`, and `error` props then you can accomplish the above more tersely by using the ES6 spread `...` operator.
```tsx
function MyComponent() {
const variants = useList([
{title: '', description: ''},
{title: '', description: ''},
]);
return (
<ul>
{variants.map((fields, index) => (
<li key={index}>
<TextField label="title" name={`title${index}`} {...fields.title} />
<TextField
label="description"
id={`description${index}`}
{...fields.description}
/>
</li>
))}
</ul>
);
}
```
##### Remarks
**Reinitialization:** If the `list` property of the field configuration changes between calls to `useList`, the field will be reset to use it as its new default value.
**Imperative methods:** The returned `Field` objects contains a number of methods used to imperatively alter their state. These should only be used as escape hatches where the existing hooks and components do not make your life easy, or to build new abstractions in the same vein as `useForm`, `useSubmit` and friends.
#### `useDynamicList()`
This offers the same functionality as useList. `useDynamicList` is a hook that adds on `useList` by adding the ability to dynamically add and remove list items. The same way you would utilize `useList` is the same way you would utilize `useDynamicList` except some differences such as the `addItem`, and the `removeItem` function. We also have to pass in a factory into this hook (telling the hook the exact type of object to add and how it should be initialized).
##### Using `useDynamicList`
Lets simulate having a user interface that allows you add as many payments methods as you wish.
1. We have to initialize `useDynamicList` the following way
```tsx
const emptyCardFactory = (): Card => ({
cardNumber: '',
cvv: '',
});
const {fields, addItem, removeItem} = useDynamicList(
[{cardNumber: '4242 4242 4242 4242', cvv: '000'}],
emptyCardFactory,
);
```
We can also have a factory that produces multiple cards such as
```tsx
const emptyCardFactory = (): Card[] => [
{
cardNumber: '',
cvv: '',
},
{
cardNumber: '',
cvv: '',
},
];
const {fields, addItem, removeItem} = useDynamicList(
[{cardNumber: '4242 4242 4242 4242', cvv: '000'}],
emptyCardFactory,
);
```
You can choose to initialize the list with an existing number of cards or no card.
2. Displaying your list and attaching the handlers `useDynamicList` provides
```tsx
{
fields.map((field, index) => (
<FormLayout.Group key={index}>
<TextField
placeholder="Card Number"
label="Card Number"
value={field.cardNumber.value}
onChange={field.cardNumber.onChange}
/>
<TextField
placeholder="CVV"
label="CVV"
value={field.cvv.value}
onChange={field.cvv.onChange}
/>
<div>
<Button onClick={() => removeItem(index)}>Remove</Button>
</div>
</FormLayout.Group>
));
}
<Button onClick={() => addItem()}>Add Card</Button>;
```
We render our UI representation of the fields by utilizing the `fields.map` function. For each field, we can use the handlers such as onChange, value, onBlur. These are the same handlers that useList provides.
We can also utilize the `removeItem`, and `addItem` functions. In this example, these functions are attached to a button press. When we remove an item we pass in the index (to indicate what field item to remove). When we add a item, we utilize the factory we passed in when initializing `useDynamicList`.
Note that addItem can also take in an argument which can then be passed onto the factory. In this case, the factory can be
```tsx
const emptyCardFactory = (factoryArgument: any): Card => ([{
cardNumber: '',
cvv: '',
});
```
You can then use this argument to do as you wish :).
#### Does this work with useForm?
Yes this works out of the box with `useForm` and is compatible with what `useList` is compatible with.
We would utilize it the following way
```tsx
const {submit, dirty, submitting} = useForm({
fields: {
fields, // the fields returned from useDynamicList.
},
onSubmit: async fieldValues => {
console.log(fieldValues);
return {status: 'success'};
},
});
```
A code sandbox of `useDynamicList` in action can be seen [here](https://codesandbox.io/s/hungry-rubin-exrkz?fontsize=14&hidenavigation=1&theme=dark)
Here, onSubmit is called when the form is submitted. See `useForm` documentation.
#### `useForm()`
A custom hook for managing the state of an entire form. `useForm` wraps up many of the other hooks in this package in one API, and when combined with `useField` and `useList`, allows you to easily build complex forms with smart defaults for common cases.
##### Signature
```tsx
const form = useForm(config);
```
###### Parameters:
- `config`, An object has the following fields:
- `fields`, A dictionary of `Field` objects, dictionaries of `Field` objects, and lists of dictionaries of `Field` objects. Generally, you'll want these to be generated by the other hooks in this package, either `useField` or `useList`. This will be returned back out as the `fields` property of the return value.
- `onSubmit`, An async function to handle submission of the form. If this function returns an object of `{status: 'fail', error: FormError[]}` then the submission is considered a failure. Otherwise, it should return an object with `{status: 'success'}` and the submission will be considered a success. `useForm` will also call all client-side validation methods for the fields passed to it. The `onSubmit` handler will not be called if client validations fails.
- `makeCleanAfterSubmit`, A boolean flag (defaults to `false`) indicating whether the form should "undirty" itself after a successful submission. If `true`, then the form fields' default values will be set to their submitted values after a successful submission. This is useful in the case where you'd want to submit the same form multiple times while "saving" the most recent submission as the new default state.
###### Return value:
An object representing the current state of the form, with imperative methods to reset, submit, validate, and clean. Generally, the returned properties correspond 1:1 with the specific hook/utility for their functionality.
#### Examples
`useForm` wraps one or more fields and return an object with all of the fields you need to manage a form.
```tsx
import React from 'react';
import {useField, useForm} from '@shopify/react-form';
function MyComponent() {
const {
fields: {title},
submit,
submitting,
dirty,
reset,
submitErrors,
makeClean,
} = useForm({
fields: {
title: useField('some default title'),
},
onSubmit: async fieldValues => {
return {status: 'fail', errors: [{message: 'bad form data'}]};
},
});
const loading = submitting ? <p className="loading">loading...</p> : null;
const submitErrorContent =
submitErrors.length > 0 ? (
<p className="error">{submitErrors.join(', ')}</p>
) : null;
const titleError = title.error ? (
<p className="error">{title.error}</p>
) : null;
return (
<form onSubmit={submit}>
{loading}
{submitErrorContent}
<div>
<label for="title">Title</label>
<input
id="title"
name="title"
value={title.value}
onChange={title.onChange}
onBlur={title.onBlur}
/>
{titleError}
</div>
<button disabled={!dirty} onClick={reset}>
Reset
</button>
<button type="submit" disabled={!dirty}>
Submit
</button>
</form>
);
}
```
As with `useField` and `useList`, `useForm` plays nicely out of the box with `@shopify/polaris`.
```tsx
import React from 'react';
import {
Page,
TextField,
FormLayout,
Card,
Layout,
Form,
ContextualSaveBar,
Frame,
Banner,
} from '@shopify/polaris';
import {useField, useForm} from '@shopify/react-form';
function MyComponent() {
const {
fields,
submit,
submitting,
dirty,
reset,
submitErrors,
makeClean,
} = useForm({
fields: {
title: useField('some default title'),
description: useField('some default description'),
},
onSubmit: async fieldValues => {
return {status: 'fail', errors: [{message: 'bad form data'}]};
},
});
const contextBar = dirty ? (
<ContextualSaveBar
message="New product"
saveAction={{
onAction: submit,
loading: submitting,
disabled: false,
}}
discardAction={{
onAction: reset,
}}
/>
) : null;
const errorBanner =
submitErrors.length > 0 ? (
<Layout.Section>
<Banner status="critical">
<p>There were some issues with your form submission:</p>
<ul>
{submitErrors.map(({message}, index) => {
return <li key={`${message}${index}`}>{message}</li>;
})}
</ul>
</Banner>
</Layout.Section>
) : null;
return (
<Frame>
<Form onSubmit={submit}>
<Page title={pageTitle}>
{contextBar}
<Layout>
{errorBanner}
<Layout.Section>
<Card>
<Card.Section>
<FormLayout>
<TextField label="Title" {...fields.title} />
<TextField
multiline
label="Description"
{...fields.description}
/>
</FormLayout>
</Card.Section>
</Card>
</Layout.Section>
</Layout>
</Page>
</Form>
</Frame>
);
}
```
You can also configure fields ahead of time and pass them in to `useForm` afterwards. This is useful when you need one field to depend upon another for validation.
```tsx
import React from 'react';
import {
Page,
TextField,
FormLayout,
Card,
Layout,
Form,
ContextualSaveBar,
Frame,
Banner,
} from '@shopify/polaris';
import {useField, useForm} from '@shopify/react-form';
function MyComponent() {
const title = useField('');
const price = useField(
{
value: '0.00',
validates: value => {
if (title.value.includes('expensive') && parseFloat(value) < 1000) {
return 'Expensive items must cost more than 1000 dollars';
}
},
},
[title.value],
);
const {submit, submitting, dirty, reset, submitErrors, makeClean} = useForm({
fields: {title, description},
onSubmit: async fieldValues => {
return {status: 'fail', errors: [{message: 'bad form data'}]};
},
});
const contextBar = dirty ? (
<ContextualSaveBar
message="New product"
saveAction={{
onAction: submit,
loading: submitting,
disabled: false,
}}
discardAction={{
onAction: reset,
}}
/>
) : null;
const errorBanner =
submitErrors.length > 0 ? (
<Layout.Section>
<Banner status="critical">
<p>There were some issues with your form submission:</p>
<ul>
{submitErrors.map(({message}, index) => {
return <li key={`${message}${index}`}>{message}</li>;
})}
</ul>
</Banner>
</Layout.Section>
) : null;
return (
<Frame>
<Form onSubmit={submit}>
<Page title={pageTitle}>
{contextBar}
<Layout>
{errorBanner}
<Layout.Section>
<Card>
<Card.Section>
<FormLayout>
<TextField label="Title" {...title} />
<TextField type="number" label="Price" {...price} />
</FormLayout>
</Card.Section>
</Card>
</Layout.Section>
</Layout>
</Page>
</Form>
</Frame>
);
}
```
##### Remarks
- **Building your own:** Internally, `useForm` is a convenience wrapper over `useDirty`, `useReset`, `validateAll`, `useSubmit`, and `propagateErrors`.If you only need some of its functionality, consider building a custom hook combining a subset of them.
- **Subforms:** You can have multiple `useForm`s wrapping different subsets of a group of fields. Using this you can submit subsections of the form independently and have all the error and dirty tracking logic "just work" together.
#### useSubmit
#### useDirty
#### useReset
Docs for these standalone hooks are coming soon. For now check out the `.d.ts` files for the API.
### Validation
Detailed validation docs are coming soon. For now check out the `.d.ts` files for the API.
### Utilities
#### reduceFields
Similar to `Array.reduce()` it visits all fields in the form
##### Signature
```tsx
function reduceFields<V>(
fieldBag: FieldBag,
reduceFn: (
accumulator: V,
currentField: Field<any>,
path: (string | number)[],
fieldBag: FieldBag,
) => V,
initialValue?: V,
): V;
```
###### Parameters:
- `fieldBag` is the collection of fields returned from `useForm`.
- `reduceFn` is the reducer function to operate on each field. The return value will be passed onto the next iteration.
- `initialValues` (optional) the starting value passed to the first iteration of the reducerFn.
- `reduceEmptyFn` (optional) is a reducer function that acts on non-field values such as empty array, empty object, and primitives.
###### Return value:
A value returned by `reduceFn` iterating through all the fields in the form, and `reduceEmptyFn` for all empty or primitive values.
##### Examples
Here is how to calculate if the entire form is dirty (though in practice you could use `useDirty` to the same effect):
```tsx
const {fields} = useForm(/* ... */);
const dirty = reduceFields(
fields,
(_dirty, field) => _dirty || field.dirty,
false,
);
```
#### getValues
Gets a form's field values like you get in `onSubmit`.
##### Signature
```tsx
function getValues<T extends object>(fieldBag: FieldBag): T;
```
###### Parameters:
- `fieldBag` is the collection of fields returned from `useForm`.
###### Return value:
Field values from the form.
##### Examples
Here is how to get the values from a nested field object:
```tsx
const {fields} = useForm({
fields: {
name: {
first: useField('your'),
last: useField('name'),
},
email: useField('your.name@shopify.com'),
},
});
const formValues = getValues(fields);
// => { name: {first: 'your', last: 'name'}, email: 'your.name@shopify.com' }
```
#### fieldsToArray
Normalizes nested fields to array of fields.
##### Signature
```tsx
function fieldsToArray(fieldBag: FieldBag): Field[];
```
###### Parameters:
- `fieldBag` is the collection of fields returned from `useForm`.
###### Return value:
Array of fields.
##### Examples
```tsx
const firstName = useField('your');
const lastName = useField('name');
const email = useField('your.name@shopify.com');
const {fields} = useForm({
fields: {
name: {
first: firstName,
last: lastName,
},
email,
},
});
const formValues = fieldsToArray(fields);
// Note: Ordering of fields is not guaranteed.
// => [firstName, lastName, email]
```
#### makeCleanFields
Resets `dirty` state for a collection of fields while maintaining the current field values. Sets all fields' default values to be their current values. Meant for use cases not covered by configuring `useForm` or `useSubmit`.
##### Signature
```tsx
function makeCleanFields(fieldBag: FieldBag): void;
```
##### Parameters
- `fieldBag` is the collection of fields returned from `useForm`.
##### Examples
```tsx
const fields = {
field1: useField('field1 default'),
field2: useField('field2 default'),
field3: useField('field3 default'),
};
const {
fields: {field1, field2, field3},
dirty,
submit,
} = useForm({
fields,
onSubmit: async values => {
console.log('You submitted the form!', values);
if (someCondition) {
makeCleanFields(fields); // set form state to 'clean'
}
return submitSuccess();
},
makeCleanAfterSubmit: false, // just for illustrative purposes, the default is false
});
```
## FAQ
**Q: Why yet another form library? Why not just include a hook version in `@shopify/react-form-state`?**
**A:** It turns out that hooks enable a much more extensible and composable paradigm than render props did. they also have some limitations that the render prop API does not. As such its difficult to build the best API we can for a hooks world and still have it match up to the old monolithic `<FormState />` model. Since we use `<FormState />` in production and will need to keep it around in maintenance mode for the foreseeable future, it makes sense to have this library available as its own import. Apart from the clean composability of hooks, the rationale otherwise remains much the same as [`<FormState />`'s](https://github.com/Shopify/quilt/blob/master/packages/react-form-state/docs/FAQ.md)
**Q: When should I use `<FormState />` from `@shopify/react-form-state` instead of this?**
**A:** `@shopify/react-form-state` is now in maintenance mode only as of the release of this library. That means you're encouraged to use this library for any new work.
**Q: How can I revalidate fields when a different field changes?**
**A:** Part of the fun of hooks is how easy it is to compose different ones together. Since our fields All have `runValidation` methods, you can easily use the built in `useEffect` hook to invoke a field's validation whenever any arbitrary value is changed.
```tsx
import React from 'react';
import {TextField} from '@shopify/polaris';
import {useField, useForm} from '@shopify/react-form';
function MyForm() {
const title = useField({
value: '',
});
const price = useField(
{
value: '2.00',
validates: value => {
if (title.value.includes('expensive') && parseFloat(value) < 1000) {
return 'Expensive items must be at least 1000 dollars.';
}
},
},
[title.value],
);
// whenever title changes we run the validator
React.useEffect(price.runValidation, [title.value]);
return (
<div>
<TextField {...title} />
<TextField {...price} />
</div>
);
}
```
**Q: I want to use this in a class component, how can I?**
**A:** The short answer is you can't. The long answer is with the magic of components. You can create a functional component that wraps your form hooks and calls a render prop with the generated fields. This approximates a component based API, and is more flexible to your specific needs than trying to have a monolithic version available as part of the library.
```tsx
import React from 'react';
import {useField, useForm} from '@shopify/react-form';
function ProductForm({children, data}) {
const title = useField(data.title);
const description = useField(data.description);
const form = useForm({
title,
description,
});
return <form onSubmit={form.submit}>{children(form)}</form>;
}
```
**Q: I want to dynamically change how many fields I render, how can I do that?**
**A:** It's tough to do this without breaking the rules of hooks. A possible solution is building a component encapsulating your hook calls, such as above, and then using a `key` to reset it when you need to. Another is 'faking' it by simply not rendering all of your fields despite always initializing the same number. Finally, you could build a custom reducer-based hook similar to `useList` that can support adding and removing fields dynamically. As long as it returns `Field` objects matching the API that the hooks in this library do, you will be able to use it with `useForm` and friends just fine.
**Q: How come {Feature X} is not supported?**
**A:** If you feel strongly about any features which are not part of this library, please open an issue or pull request.