3.2 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use subscribe() to React to Form Changes Outside the React Lifecycle | HIGH | eliminates re-renders for non-UI consumers like analytics, autosave, telemetry | sub, subscribe, side-effects, analytics, autosave, useForm |
Use subscribe() to React to Form Changes Outside the React Lifecycle
Introduced in v7.55.0, useForm().subscribe(...) registers a callback that fires on form state or value changes without causing any re-renders. Use it when the consumer of the change is not a UI element — analytics, autosave to localStorage, debounced telemetry, sending drafts to a server. useWatch and watch are still right for things that paint to screen; subscribe is right for everything else.
Incorrect (using useWatch to drive a non-UI side-effect — re-renders the form on every keystroke):
function ProfileForm() {
const { register, handleSubmit, control } = useForm<ProfileFormData>()
const values = useWatch({ control }) // Every keystroke re-renders ProfileForm
useEffect(() => {
analytics.track('profile_field_edited', { values }) // Fires on every render
}, [values])
return (
<form onSubmit={handleSubmit(saveProfile)}>
<input {...register('displayName')} />
<input {...register('bio')} />
</form>
)
}
Correct (subscribe() runs the side-effect with zero re-renders):
function ProfileForm() {
const { register, handleSubmit, subscribe } = useForm<ProfileFormData>()
useEffect(() => {
const unsubscribe = subscribe({
formState: { values: true },
callback: ({ values, name }) => {
analytics.track('profile_field_edited', { field: name, values })
},
})
return unsubscribe
}, [subscribe])
return (
<form onSubmit={handleSubmit(saveProfile)}>
<input {...register('displayName')} />
<input {...register('bio')} />
</form>
)
}
Subscribing to specific fields with formState slices (e.g. dirty-aware autosave):
function DraftEditor() {
const { register, subscribe } = useForm<DraftFormData>({
defaultValues: loadDraft(),
})
useEffect(() => {
const unsubscribe = subscribe({
name: ['title', 'body'],
formState: { values: true, isDirty: true },
callback: ({ values, isDirty }) => {
if (isDirty) debouncedSaveDraft(values)
},
})
return unsubscribe
}, [subscribe])
return (
<>
<input {...register('title')} />
<textarea {...register('body')} />
</>
)
}
When to use which:
useWatch/Controller— the value drives a rendered elementsubscribe— the value drives a non-UI side-effect (analytics, autosave, localStorage sync, telemetry)watch(callback)— legacy callback form; prefersubscribein new code (subscribe replaces the watch-callback pattern with explicit formState slicing and no implicit re-renders)
subscribe returns an unsubscribe function — always return it from the useEffect cleanup to avoid leaks across remounts.
Reference: subscribe · Release notes v7.55.0