subscribe
Subscribe to form state changes outside the render cycle.
</> subscribe: UseFormSubscribe Since v7.55.0
Subscribe to formState changes and value updates. You can subscribe to individual fields or the entire form, while avoiding unnecessary re-renders caused by form changes.
Props
| Name | Type | Description | Example |
|---|---|---|---|
| name | undefined | Subscribe to the entire form | subscribe({ callback }) |
| string | Subscribe to a single field by name. | subscribe({ name: 'firstName' }) | |
| string[] | Subscribe to multiple fields by name. | subscribe({ name: ['firstName', 'lastName'] }) | |
| formState | Partial<ReadFormState> | Pick which formState to subscribe to. |
|
| callback | Function | Runs whenever any of the picked formState changes. It receives the full form state, plus values (the latest form values), name (the field that changed, if any) and type (the event, such as change or blur). |
|
| exact | boolean = false | Enable exact name matching. When false (default), the subscription fires when the subscribed name is a prefix of the changed field name, or vice versa. Since v7.81.0 When true, it fires when the subscribed field itself or one of its ancestor paths is updated, but not when a nested child path changes (for example, "users.0.name" is updated when "users" is set, while "users" is not updated when "users.0.name" changes). | subscribe({ name: 'target', exact: true }) |
Return
| Type | Description |
|---|---|
() => void | A function that ends the subscription. Call it when the component unmounts, for example by returning it from useEffect. |
- This function shares the same functionality as
createFormControl.subscribe, with the key difference being that createFormControl can be initialized outside of a React component. - This function is dedicated to subscribing to form state without render, use this function for that instead of watch callback function.
Examples
import { useEffect } from "react"import { useForm } from "react-hook-form"type FormInputs = {firstName: stringlastName: string}export default function App() {const { register, subscribe } = useForm<FormInputs>()useEffect(() => {// make sure to unsubscribe;const callback = subscribe({formState: {values: true,},callback: ({ values }) => {console.log(values)},})return () => callback()// You can also just return the subscribe// return subscribe({ callback: ({ values }) => console.log(values) });}, [subscribe])return (<form><input {...register("firstName", { required: true })} /><input {...register("lastName", { required: true })} /></form>)}
Autosave a draft
Because subscribe doesn't re-render, it suits side effects like saving a draft while the user types:
import { useEffect } from "react"import { useForm } from "react-hook-form"type FormValues = { title: string; body: string }export default function App() {const { register, subscribe } = useForm<FormValues>({defaultValues: { title: "", body: "" },})useEffect(() => {let timer: ReturnType<typeof setTimeout>const unsubscribe = subscribe({formState: { values: true },callback: ({ values }) => {clearTimeout(timer)timer = setTimeout(() => {localStorage.setItem("draft", JSON.stringify(values))}, 500)},})return () => {clearTimeout(timer)unsubscribe()}}, [subscribe])return (<form><input {...register("title")} /><textarea {...register("body")} /></form>)}
Tips
-
Pick only the
formStatekeys you need. The callback runs for each update to any of them, so a smaller selection means less work. -
Pass
nameto limit the subscription to one part of the form, such assubscribe({ name: "shipping", ... }). -
The callback can run on every key press. Debounce expensive work, such as saving to the server:
useEffect(() => {const save = debounce((values) => api.saveDraft(values), 500)const unsubscribe = subscribe({formState: { values: true },callback: ({ values }) => save(values),})return () => {unsubscribe()save.cancel()}}, [subscribe])
Troubleshooting
The callback does not receive value changes
Why it happens
subscribe only listens to the form-state properties selected in formState. Without values: true, changes to input values alone do not trigger a values subscription.
Fix
Select values explicitly, and optionally limit the work to a field or section:
return subscribe({name: "shipping",formState: { values: true },callback: ({ values }) => save(values.shipping),})
The callback runs multiple times for one change
Why it happens
A new subscription is being created on each render, or an effect creates one without removing the previous subscription.
Fix
Create the subscription in an effect and return the unsubscribe function:
useEffect(() =>subscribe({formState: { values: true },callback: ({ values }) => console.log(values),}),[subscribe])