REACT Contents

Controlled vs Uncontrolled Abstractions

Controlled mode enables external orchestration and URL sync, while uncontrolled mode reduces wiring. Learn decision rules, dual mode design, and failure modes like drift and double sources of truth.

On this page

What Controlled and Uncontrolled Mean

  • Controlled: parent owns state and passes value plus onChange.
  • Uncontrolled: component owns internal state, parent configures via defaults.
  • Production rule: choose based on ownership, coordination needs, and persistence requirements.

When Controlled Mode Is Required

  • State must sync to URL, storage, or server state.
  • Multiple components must coordinate the same value.
  • Parent must enforce invariants and validation.
  • Testing requires explicit state control.

When Uncontrolled Mode Is Better

  • Local interaction state that does not affect siblings.
  • Simple components where wiring value and onChange would add noise.
  • Performance sensitive inputs where parent rerenders are expensive.

Dual Mode Design Rules

  • Support both modes only if there is a clear product need.
  • Use an explicit controlled check: value prop provided means controlled.
  • Keep one source of truth at a time. Do not mix internal and external state.
  • Always call onChange when user intent changes value, even in uncontrolled mode if provided.

Example: Dual Mode Hook

function useControllableState<T>({
  value,
  defaultValue,
  onChange,
}: {
  value?: T;
  defaultValue: T;
  onChange?: (v: T) => void;
}) {
  const [internal, setInternal] = React.useState<T>(defaultValue);
  const isControlled = value !== undefined;
  const current = isControlled ? (value as T) : internal;

  const set = React.useCallback((next: T) => {
    if (!isControlled) setInternal(next);
    if (onChange) onChange(next);
  }, [isControlled, onChange]);

  return [current, set] as const;
}

Example: Component Using Dual Mode

function Toggle({
  value,
  defaultValue = false,
  onChange,
}: {
  value?: boolean;
  defaultValue?: boolean;
  onChange?: (v: boolean) => void;
}) {
  const [v, setV] = useControllableState({
    value,
    defaultValue,
    onChange,
  });

  return (
    <button onClick={() => setV(!v)} aria-pressed={v}>
      {v ? "On" : "Off"}
    </button>
  );
}

Operational Failure Modes

  • Drift: internal state and external props disagree due to mixed ownership.
  • Lost updates: onChange called but parent does not update value in controlled mode.
  • Remount reset: uncontrolled state resets when keys change or route remount occurs.
  • Double validation: both parent and child enforce different rules.

Debug Playbook

  • If UI does not update in controlled mode, check parent state update path.
  • If state resets unexpectedly, inspect keys and route boundaries.
  • If value oscillates, search for two sources of truth writing in different places.

Code Review Checklist

  • Controlled detection is explicit and consistent.
  • No internal state writes in controlled mode.
  • onChange is always fired on user intent changes.
  • Default values are only used in uncontrolled mode.