What is Controlled component?

Controlled component: A controlled component is a React input whose value is held in state and passed down as a prop, with onChange reporting edits, so state is the source of truth.

In React, an input is controlled when its value comes from a prop and every edit goes through an onChange handler that updates state, which flows back into the prop. The input shows whatever state says; typing does not change it until state does. An uncontrolled input keeps its own value in the DOM, like a plain HTML form field, and React reads it when needed through a ref or FormData. The distinction applies to every stateful component, not just text fields: a dialog's open, a tab list's value and a switch's checked can each be controlled or uncontrolled.

Which to use

  • Controlled when something else depends on the value as it changes: live validation, filtering a list, enabling a button, a character counter, formatting as you type, or syncing with the URL.
  • Uncontrolled when you only need the value on submit. Native validation attributes, browser autofill and React 19 form actions all work with uncontrolled fields, and there is no re-render per keystroke.
  • Controlled from the URL or a store for filters and view settings that should survive a reload and be shareable.

How to build one in React

A controlled field pairs value with onChange. For a reusable component, support both modes the way MiniDev's components do: use the value prop when it is given, otherwise fall back to internal state seeded from defaultValue:

tsx
"use client"
import * as React from "react"

type Props = {
  value?: string
  defaultValue?: string
  onChange?: (value: string) => void
}

export function useControllable({ value, defaultValue = "", onChange }: Props) {
  const [internal, setInternal] = React.useState(defaultValue)
  const controlled = value !== undefined
  const current = controlled ? value : internal
  const set = React.useCallback(
    (next: string) => {
      if (!controlled) setInternal(next)
      onChange?.(next)
    },
    [controlled, onChange]
  )
  return [current, set] as const
}

export function NameField(props: Props) {
  const [name, setName] = useControllable(props)
  return <input value={name} onChange={(e) => setName(e.target.value)} aria-label="Name" />
}

This is the pattern behind SegmentedControl, MultiSelect and Switch: value or checked when the parent owns the state, defaultValue or defaultChecked when it does not, and the same change handler either way. Base UI primitives follow the same convention with value and onValueChange.

Common mistakes

  • Switching modes. Passing value={undefined} on one render and a string on the next makes React warn about a component changing from uncontrolled to controlled. Use an empty string for "no value".
  • A `value` with no `onChange`. The field becomes read-only and React warns. Use readOnly if that is intended.
  • Slow renders on every keystroke. A controlled field re-renders its owner on each edit. Keep the state close to the input, or debounce the expensive work rather than the value.
  • Resetting on remount. Uncontrolled fields lose their value when the component unmounts. Give the form a key to reset deliberately, and keep draft state in a parent if it must survive.

Every input in the forms category accepts both modes, so you can start uncontrolled and lift state later without changing the markup.

InputuiThe base text input with sm, default and lg sizes and designed hover, focus, disabled, read only and invalid states. A drop in shadcn style input for React.npx shadcn@latest add @minidev/inputSwitchuiSwitch on Base UI whose thumb stretches on press then springs across, with sm and default sizes, a loading spinner state and an optional clickable label row.npx shadcn@latest add @minidev/switch