# Listeners

For side effects of a value change, use `onValueChanged`:

```ts
form.field("country", {
  onValueChanged() {
    form.field("province").value = "";
  },
});
```

It fires whenever `value` settles to something new, from any source, but not for
the initial value seeded at construction. Like `validators`/`dependents`, it's
refreshed on every `field` call, so a caller passing a fresh closure each time
(e.g. a React re-render) never invokes a stale one. This works identically on
any node (leaf, nested field, or the form root) since it's a plain
`FieldApiOptions` option, not something bolted onto leaves specifically.

## At the form root: persisting form state

Since every nested field's change bubbles up into the root's own `value`, one
`onValueChanged` on the form reacts to _any_ edit in the tree, without wiring a
handler onto each individual field:

<CodeGroup>

<CodeGroupItem label="React">

```ts
const form = useForm({
  initialValue: draft,
  onValueChanged: (form) => {
    localStorage.setItem("draft", JSON.stringify(form.value));
  },
});
```

</CodeGroupItem>

<CodeGroupItem label="Lit">

```lit
#form = new FormApi({
  initialValue: draft,
  onValueChanged: (form) => {
    localStorage.setItem("draft", JSON.stringify(form.value));
  },
});
```

</CodeGroupItem>

</CodeGroup>

## Debouncing

`onValueChanged` fires on every settled change: for a text input bound via
`handleChange`, that's every keystroke. Unlike `asyncValidator`
(`validationDebounceMs`), there's no built-in debounce for `onValueChanged`:
it's a synchronous, no-return-value callback, so debouncing it is just wrapping
it in a timer. That's left to you on purpose. Trailing vs. leading edge,
`maxWait`, and so on are real choices a single built-in policy wouldn't fit
everyone.

<CodeGroup>

<CodeGroupItem label="React">

```tsx
// Memoize the debounced function so it survives useForm's every-render
// updateOptions refresh instead of resetting its timer on every keystroke.
const persist = useMemo(
  () => debounce((form: FormApi<Draft>) => saveDraft(form.value), 500),
  [],
);
const form = useForm({
  initialValue: draft,
  onValueChanged: persist,
});
```

</CodeGroupItem>

<CodeGroupItem label="Lit">

```lit
// A stable class field, unlike a React hook's fresh closure every render, so
// no memoization is needed to keep the debounce timer alive.
#form = new FormApi({
  initialValue: draft,
  onValueChanged: debounce(
    (form) => localStorage.setItem("draft", JSON.stringify(form.value)),
    500,
  ),
});
```

</CodeGroupItem>

</CodeGroup>

## Listening for `touched`/`invalid`/`validating`

There's no dedicated callback for those: `onValueChanged` is deliberately scoped
to values, the case that comes up most in practice (validation side effects,
persistence). To react to a different property, subscribe directly; see
[Reactivity](/form/guide/reactivity).

<CodeGroup>

<CodeGroupItem label="React">

```tsx
const touched = useWatch(field, (f) => f.touched);

useEffect(() => {
  if (touched) reportFieldTouched(field.name);
}, [touched]);
```

</CodeGroupItem>

<CodeGroupItem label="Lit">

```lit
#unsubscribe?: VoidFunction;

override connectedCallback() {
  super.connectedCallback();
  this.#unsubscribe = field.subscribe(() => {
    if (field.touched) reportFieldTouched(field.name);
  });
}

override disconnectedCallback() {
  super.disconnectedCallback();
  this.#unsubscribe?.();
}
```

</CodeGroupItem>

</CodeGroup>
