onChange debounce is never cancelled on unmount, so JsonForms emits after the form is gone

Open Beginner friendly
#2,616 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
react, typescript
Domain
frontend

Research direction

Start in packages/react/src/JsonFormsContext.tsx around JsonFormsStateProvider and the debounced onChange emit at lines 258-263. Read the existing debounce and effect lifecycle, then verify the relevant React package tests or reproduce the reported unmount sequence. Done means unmounting within the 10 ms window produces no later onChange callback and avoids the reported React 19/jsdom teardown error.

Written by the indexing model from the issue text.

Description

react
Describe the bug

JsonFormsStateProvider debounces its onChange emit by 10 ms but never cancels the pending timer when the component unmounts. A trailing edge scheduled just before unmount still fires afterwards, invoking the consumer's onChange when the form is already gone.

https://github.com/eclipsesource/jsonforms/blob/master/packages/react/src/JsonFormsContext.tsx#L258-L263

const debouncedEmit = useCallback(
  debounce((...args: any[]) => onChangeRef.current?.(...args), 10),
  []
);
useEffect(() => {
  debouncedEmit({ data: core.data, errors: core.errors });
}, [core.data, core.errors]);

There is no cleanup effect, and nothing calls debouncedEmit.cancel().

The debounce itself is well-motivated (#1150, and the comment above it explains the Chrome-autofill rerender chain). This report is only about the missing cancel on unmount.

Expected behavior

Once <JsonForms> unmounts, it should not call onChange again. A consumer that unmounts a form cannot reasonably expect a change callback for it up to 10 ms later.

Steps to reproduce the issue
  1. Render <JsonForms … onChange={spy} />.
  2. Fire a change on any control.
  3. Unmount within the 10 ms debounce window.
  4. Wait > 10 ms — spy receives one more call, after unmount.
Impact we hit

Two flavours, one benign and one not:

  • In the browser: a state update and a parent callback up to 10 ms after unmount. Usually harmless, but it is a real "write after teardown" — for us it meant a parent's onDataChange firing for a form the user had already navigated away from.
  • In tests (the expensive one): under React 19 + jsdom, the late emit reaches React's dispatchSetState after the test environment is torn down, throwing an unhandled ReferenceError: window is not defined. Vitest reports this as an unhandled error, so the run fails while every test passes — a red CI with a green test summary, which is very hard to attribute.
ReferenceError: window is not defined
  ❯ resolveUpdatePriority  react-dom-client.development.js
  ❯ dispatchSetState       react-dom-client.development.js
  ❯ <consumer onChange handler>
  ❯ packages/react/lib/jsonforms-react.cjs.js:217
  ❯ invokeFunc → trailingEdge → Timeout.timerExpired   lodash/debounce.js

Consumers can work around it by guarding their handler with an isMounted ref (that is what we did), but every consumer has to know this independently.

Suggested fix

Cancel on unmount:

useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);

If flushing is preferred over dropping, debouncedEmit.flush() would at least run it while the tree is still alive — though for the jsdom case dropping is the safer default.

(Side note, unrelated to the bug: useCallback(debounce(...), []) constructs a new debounced function on every render and discards it, so the linter's exhaustive-deps rule is being worked around; useMemo/useRef would express the intent more directly.)

Environment
  • JSON Forms: 3.8.0 (latest) — also present on master and in 3.9.0-alpha.1, checked 2026-08-10
  • Framework: React 19.2
  • Renderer set: custom (Mantine) — but this is framework-agnostic, it is in @jsonforms/react core
Dominant language
TypeScript
Stars
2.7k
Forks
423
Avg merge
17d 8h
Merged PRs (30d)
1

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from eclipsesource/jsonforms

All issues in eclipsesource/jsonforms

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.