30% offEnding soon
ControllableInputLoading saved progress…

ControllableInput

A controllable component supports either parent-owned state or its own local state through one public API. Implement ControllableInput, a forwardRef wrapper around a native input whose mode is selected once on mount. It must route reserved state props itself while forwarding ordinary input attributes.

Signature

type ControllableInputProps = Omit<
  React.InputHTMLAttributes<HTMLInputElement>,
  'value' | 'defaultValue' | 'onChange' | 'children'
> & {
  value?: string | number | readonly string[] | null;
  defaultValue?: string | number | readonly string[] | null;
  onChange?: React.ChangeEventHandler<HTMLInputElement>;
};

const ControllableInput: React.ForwardRefExoticComponent<
  ControllableInputProps & React.RefAttributes<HTMLInputElement>
>;

Examples

Passing a defined value selects controlled mode. The parent must feed the next value back:

function NameField() {
  const [name, setName] = React.useState('Ada');
  return React.createElement(ControllableInput, {
    value: name,
    onChange: (event) => setName(event.target.value),
  });
}

Omitting value selects uncontrolled mode. The component owns later edits:

React.createElement(ControllableInput, {
  defaultValue: 'draft',
  onChange: (event) => console.log(event.target.value),
  placeholder: 'Title',
});

Notes

  • Mode is fixed on the first mount. The component is controlled exactly when its first value is not undefined. null therefore selects controlled mode and renders as an empty string.
  • Controlled means parent-owned. Render the latest value ?? '', call onChange(event), and never copy the typed value into internal state.
  • Uncontrolled means locally owned. Initialize once from defaultValue ?? '', update local state from event.target.value, and still call onChange(event).
  • Switch attempts are ignored. A controlled instance stays controlled even if a later value is undefined; an uncontrolled instance ignores a later value prop. Later defaultValue changes never reset local edits.
  • Forward the platform surface. Forward the ref and ordinary native input props. Do not pass value, defaultValue, or onChange through blindly, and do not pass children because input is a void element.
  • Out of scope. Do not add validation, debouncing, persistence, form-library integration, textarea/select support, or custom imperative methods.