Skip to content

fix: restore controlled form controls without a change handler - #117

Draft
bilalvur wants to merge 4 commits into
TanStack:mainfrom
bilalvur:fix/restore-controlled-without-handler
Draft

bilalvur wants to merge 4 commits into
TanStack:mainfrom
bilalvur:fix/restore-controlled-without-handler

Conversation

@bilalvur

Copy link
Copy Markdown

Refs #115. Stacked on #116; this covers its first two "Not covered" items. Until #116 merges, this diff includes its commits; review only the last one (cd5644a).

React restores after every change event, handler or not (createAndAccumulateChangeEvent calls enqueueStateRestore), so a controlled control without onChange stays read-only and a click on a handler-less radio restores its group. #116 only marked the event for a restore from Redact listeners on the control, so both kept the edit.

What changes

  • Once a control is controlled (checked for checkbox and radio, value otherwise), recording its props adds one shared listener that marks the event as a handler does, also on hydration. fix: restore controlled form controls after onChange #116's container listeners then restore it. Uncontrolled controls get none.
  • React's dev warning for value without onChange has no Redact counterpart; this adds none.

Size

+12 to +57 B gzip against #116; two entries go over budget. UNHANDLED_RESTORE_SIZE_ALLOWANCE holds the measured increments and needs your approval.

Not covered

  • Clicking an uncontrolled radio without handlers does not restore controlled radios in its group. React does.

Testing

On #116 the 7 new jsdom tests for handler-less controls fail, as do the handler-less input and radio browser tests; an eighth jsdom test guards against restoring uncontrolled inputs. Browser results hold in Chromium, Firefox and WebKit, and pnpm test:pr passes on Node 24.22.0.

I used Claude Code to write the fix and tests, and had it run the suites and a separate review pass.

bilalvur and others added 4 commits October 30, 2026 17:37
React writes a controlled control's value and checked state back after
its change handlers run. Redact only synced them on re-render, so a
handler that rejected an edit by not calling setState left the typed
text, checkbox toggle, radio selection or select choice in the DOM.

The restore runs after the element's last Redact listener for the
native input or change event, so onChange, onInput and their capture
forms all read the edit, also with real user input, which runs
microtasks between listeners. It is queued behind the handler's own
updates (microtask, or act's queue), so it reads the props they render
and keeps the caret of an accepted edit. Hydration records the props
too, and a radio restores the rest of its group.

Fixes TanStack#115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These controls fire input before change. With onInput and onChange both
set, the restore ran after the input listener, and since real clicks
run microtasks between events, onChange read the restored state
instead of the click. A change listener now owns the restore for them;
an onInput alone still restores after input.

Refs TanStack#115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restore ran in a microtask after the control's last Redact listener,
and real input runs microtasks between listeners. So an ancestor's
onInput or a later native listener read the restored value, and an
ancestor that accepted the edit lost it. A capture listener that
stopped propagation also skipped the restore: the target's bubble
listener, which owned it, never ran.

The control's listeners now mark the native event, and listeners on the
root and portal containers restore once it reaches them, as React's
root listener does. A stopped or non-bubbling event restores at once,
since it never reaches a container. Listening on the container rather
than the document keeps the restore when a native listener on the
container stops propagation, and covers shadow roots.

A checkbox, radio or select with a change listener still restores after
change; with only onInput it restores after input.

Refs TanStack#115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
React restores a controlled control after any change event, whether or
not it has handlers, so a control with value or checked and no onChange
stays read-only. Redact only marked the event for a restore from its
own listeners on the control. A handler-less controlled input stayed
editable, and clicking a controlled radio without onChange changed its
group even when its siblings had one.

Recording a control's props now adds one shared listener that marks the
event like a handler does, once the control is controlled, so
uncontrolled controls pay nothing. The container listeners from TanStack#116
then restore it after every listener has run.

Redact has no dev warning for value without onChange; this adds none.

Refs TanStack#115.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cd5644a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/redact Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 30, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant