Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/components/TabGroup.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.buttonTabGroup {
display: grid;
grid-auto-flow: column;
display: flex;
flex-wrap: wrap;
gap: 3px;
}

.buttonTabGroup > button {
Expand All @@ -12,10 +13,7 @@
transition: var(--transition);
text-transform: uppercase;
letter-spacing: 0.05em;
}

.buttonTabGroup > button:nth-child(n + 2) {
margin-left: 3px;
flex: 1 1 auto;
}

.buttonTabGroup > button:hover {
Expand Down
22 changes: 16 additions & 6 deletions src/content/docs/createFormControl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,24 @@ This function creates the entire form state subscription and allows you to subsc
</Admonition>

<Admonition type="important" title="Rules">
- You should either use this API or the Context API
```tsx
const props = createFormControl()

<FormProvider {...props} /> // ❌ You don't need a provider
- You should either use this API or the Context API

```tsx
const props = createFormControl()

<FormProvider {...props} /> // ❌ You don't need a provider

<input {...props.register("name")} /> // ✅ Use method directly from createFormControl
```

- Create the control once. Passing a new instance to `useForm({ formControl })` re-initializes the form. A control created at module scope is a single form shared by every component that uses it and, during server rendering, by every request. For one form per component instance, create it lazily:

```tsx
const [form] = useState(() => createFormControl({ defaultValues }))
const { register } = useForm({ formControl: form.formControl })
```

<input {...props.register('name')} /> // ✅ Use method directly from createFormControl
```
</Admonition>

##### Examples:
Expand Down
118 changes: 88 additions & 30 deletions src/content/docs/usecontroller.mdx

Large diffs are not rendered by default.

91 changes: 52 additions & 39 deletions src/content/docs/usecontroller/controller.mdx

Large diffs are not rendered by default.

90 changes: 89 additions & 1 deletion src/content/docs/usefieldarray.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function FieldArray() {

---

<TabGroup buttonLabels={["useFieldArray", "Nested Form", "conditional Field Array", "Focus Name/index"]}>
<TabGroup buttonLabels={["useFieldArray", "Edit with update", "conditional Field Array", "Focus Name/index", "Nested Field Array"]}>

<div>

Expand Down Expand Up @@ -477,6 +477,92 @@ const App = () => {
}
```

```tsx copy
import {
useForm,
useFieldArray,
Control,
UseFormRegister,
} from "react-hook-form"

type FormValues = {
teams: {
name: string
members: { email: string }[]
}[]
}

function Members({
teamIndex,
control,
register,
}: {
teamIndex: number
control: Control<FormValues>
register: UseFormRegister<FormValues>
}) {
const { fields, append, remove } = useFieldArray({
control,
name: `teams.${teamIndex}.members`,
})

return (
<ul>
{fields.map((field, memberIndex) => (
<li key={field.id}>
<input
{...register(`teams.${teamIndex}.members.${memberIndex}.email`)}
/>
<button type="button" onClick={() => remove(memberIndex)}>
Remove member
</button>
</li>
))}
<button type="button" onClick={() => append({ email: "" })}>
Add member
</button>
</ul>
)
}

export default function App() {
const { control, register, handleSubmit } = useForm<FormValues>({
defaultValues: {
teams: [{ name: "", members: [{ email: "" }] }],
},
})
const { fields, append, remove } = useFieldArray({
control,
name: "teams",
})

return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{fields.map((field, teamIndex) => (
<fieldset key={field.id}>
<input {...register(`teams.${teamIndex}.name`)} />
<Members
teamIndex={teamIndex}
control={control}
register={register}
/>
<button type="button" onClick={() => remove(teamIndex)}>
Remove team
</button>
</fieldset>
))}
<button
type="button"
onClick={() => append({ name: "", members: [{ email: "" }] })}
>
Add team
</button>
<input type="submit" />
</form>
)
}
```

</TabGroup>

### Video
Expand Down Expand Up @@ -534,6 +620,8 @@ function App() {

There will be cases where you want to control the entire field array, which means each onChange reflects on the `fields` object.

If you only need to display values from the array, such as a live total, read them with [`useWatch`](/docs/usewatch) (`useWatch({ control, name: "fieldArray" })`) in a child component instead, so only that component re-renders.

```tsx sandbox="https://codesandbox.io/s/infallible-bush-c92l0?file=/src/App.tsx"
import { useForm, useFieldArray } from "react-hook-form"

Expand Down
31 changes: 31 additions & 0 deletions src/content/docs/useform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,21 @@ useForm({

- You **should avoid** providing `undefined` as a default value, as it conflicts with the default state of a controlled component.
- `defaultValues` are cached. To reset them, use the [reset](/docs/useform/reset) API.
- An async `defaultValues` function runs once, when the form mounts. If its promise rejects, `formState.isLoading` stays `true`, so catch errors inside the function and return fallback values:

```javascript
useForm({
defaultValues: async () => {
try {
const res = await fetch("/api/user")
return await res.json()
} catch {
return { firstName: "", lastName: "" }
}
},
})
```

- `defaultValues` will be included in the submission result by default.
- It's recommended to avoid using custom objects containing prototype methods, such as `Moment` or `Luxon`, as `defaultValues`.
- There are other options for including form data:
Expand Down Expand Up @@ -219,6 +234,22 @@ function App() {
}
```

<Admonition type="important" title="Rules">

- `values` is deep-compared with the previous `values` prop. When it changes, the form is reset with the new values, which **replaces anything the user has typed**. If your data source refetches in the background (for example, React Query on window focus) and returns updated data, set `resetOptions: { keepDirtyValues: true }` to keep the user's edits.
- Passing a new object with the same content does nothing, so you don't need to memoize `values`.

</Admonition>

##### Loading form data from a server: which option to use?

| Approach | Updates when the data changes | Good for |
| --------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Async [`defaultValues`](#defaultValues) | No, it runs once on mount | Edit forms that load their data once. `formState.isLoading` is `true` until it resolves. |
| [`values`](#values) | Yes, it resets the form whenever the data changes | Forms that must stay in sync with external or server state. Combine with [`resetOptions`](#resetOptions) to keep user edits. |
| [`reset`](/docs/useform/reset) in `useEffect` | Only when your effect runs | Full control over when the form is refilled and which state to keep. |
| Render the form after the data arrives | No | The simplest option: fetch in the parent and pass the data in as `defaultValues`. |

#### errors: <TypeText>FieldErrors</TypeText> <Since version="7.49.0" /> {#errors}

---
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/useform/formstate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ This object contains information about the entire form state. It helps you keep
| `defaultValues` | <TypeText>object</TypeText> | <Since version="7.37.0" /> The value that was set at [useForm](/docs/useform)'s `defaultValues` or updated `defaultValues` via the [reset](/docs/useform/reset) API. |
| `isSubmitted` | <TypeText>boolean</TypeText> | Set to `true` after the form is submitted. Will remain `true` until the `reset` method is invoked. |
| `isSubmitSuccessful` | <TypeText>boolean</TypeText> | Indicates that the form was successfully submitted without any runtime error. |
| `isSubmitting` | <TypeText>boolean</TypeText> | `true` if the form is currently being submitted. `false` otherwise. |
| `isSubmitting` | <TypeText>boolean</TypeText> | `true` if the form is currently being submitted. `false` otherwise.<ul><li>It stays `true` only while the promise returned by your `onSubmit` handler is pending. If you start a request without `await`ing or returning it, `isSubmitting` turns `false` straight away.</li></ul> |
| `isLoading` | <TypeText>boolean</TypeText> | <Since version="7.41.0" /> `true` if the form is currently loading async default values.<ul><li>**Important:** this prop is only applicable to async `defaultValues`<CodeArea withOutCopy rawData={`const {\n formState: { isLoading }\n} = useForm({\n defaultValues: async () => await fetch('/api')\n})`}/></li></ul> |
| `submitCount` | <TypeText>number</TypeText> | The number of times the form has been submitted. |
| `isValid` | <TypeText>boolean</TypeText> | Set to `true` if the form doesn't have any errors.<ul><li>`setError` immediately forces `isValid` to `false`; this value is not itself derived from validation and will be overwritten the next time validation runs (e.g. on the next `onChange`, submit, or `trigger()` call).</li></ul> |
| `isValid` | <TypeText>boolean</TypeText> | Set to `true` if the form doesn't have any errors.<ul><li>While `isValid` is subscribed, every field (or the whole [resolver](/docs/useform#resolver) schema) is validated on mount and on each change, whatever the [`mode`](/docs/useform#mode). `errors` only fills in according to `mode` and `reValidateMode`, so with the default `mode: "onSubmit"` `isValid` can be `false` while `errors` is still empty.</li><li>`isValid` is not computed while the form is [`disabled`](/docs/useform#disabled).</li><li>`setError` immediately forces `isValid` to `false`; this value is not itself derived from validation and will be overwritten the next time validation runs (e.g. on the next `onChange`, submit, or `trigger()` call).</li></ul> |
| `isValidating` | <TypeText>boolean</TypeText> | Set to `true` during validation. |
| `validatingFields` | <TypeText>object</TypeText> | <Since version="7.51.0" /> Captures fields that are undergoing asynchronous validation. |
| `errors` | <TypeText>object</TypeText> | An object with field errors. There is also an [ErrorMessage](/docs/useformstate/errormessage) component to retrieve error messages easily. |
Expand Down
2 changes: 2 additions & 0 deletions src/content/docs/useform/getvalues.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ The example below shows what to expect when you invoke `getValues` method.
<Admonition type="important" title="Rules">

- It will return `defaultValues` from `useForm` before the **initial** render.
- `getValues` returns the raw field values. The data passed to your `handleSubmit` callback can differ: with a [resolver](/docs/useform#resolver) it's the resolver's output (with any transforms or coercion applied), and disabled fields are left out.
- `getValues` doesn't subscribe to changes, so it won't re-render your component. To render a value, use [`useWatch`](/docs/usewatch) or [`watch`](/docs/useform/watch).

</Admonition>

Expand Down
Loading
Loading