Skip to content

useControlledHostValueProps makes all eight field components flip uncontrolled to controlled #3027

Description

@mfal

useControlledHostValueProps always returns a value key, so every field built on it starts uncontrolled and becomes controlled on the first change. react-stately's useControlledState warns on exactly that transition.

Root cause

useControlledHostValueProps.ts:23:

const [value, setValue] = useState(regularValue ?? defaultValue);
// ...
return { ...props, value, onChange };

With neither value nor defaultValue set, value is undefined — react-stately reads that as uncontrolled. The first onChange calls setValue(v), value becomes defined, and the component is now controlled.

The spread is unconditional, so the value key is present even when the caller passed no value at all. That is what makes it systemic rather than per-component.

Blast radius

All eight components using the hook:

NumberField, TextField, TextArea, SearchField, MarkdownEditor, PasswordCreationField, CodeEditor, DateRangePicker

A Storybook sweep saw the warning in 22 stories, among them:

  • form-controls-numberfield--default, --with-field-error, --with-field-description, --with-contextual-help
  • form-controls-markdowneditor--default, --auto-resizeable, --show-character-count, --with-on-change, --with-ref
  • form-controls-passwordcreationfield--with-copy-button, --with-custom-button
  • form-controls-daterangepicker--with-default-presets, --with-custom-presets
  • integrations-react-hook-form-numberfield--default, --with-field-error
  • chat-chat--with-markdown-editor, structure-combine-input-button--with-number-field

The sweep only clicked, it never typed. TextField, TextArea and SearchField warn on keyboard input, so the real story count is higher.

Not a duplicate

This is the third, largest site of the same class.

Direction

useControlledState treats only undefined as uncontrolled, so the fix is to keep each field on one side of that line for its whole lifetime — either omit the value key entirely when the caller is uncontrolled, or commit to controlled from the first render. Which one is right depends on how the remote-value marker path is meant to behave, so it needs a look rather than a mechanical swap.

Note the hook lives in lib/remote/ and exists to protect remote text inputs from interleaved host/remote updates — any fix has to keep that working.

Reproducing

pnpm nx dev components

Open iframe.html?id=form-controls-numberfield--default&viewMode=story, click the stepper, and read the console.

Found in a Playwright console sweep over all 443 stories (two full passes, identical results).

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions