Controlled vs Uncontrolled
Controlled: the parent owns the value (state) and passes value + onChange. Uncontrolled: the DOM holds the value; parent reads via ref when needed (e.g. submit). Choose per component and support both when building reusable inputs.
The problem I keep seeing
Form inputs need a source of truth. If the parent keeps it in state and passes value and onChange, every keystroke triggers a re-render and the parent can validate or transform. If the DOM holds the value and the parent reads it only on submit (via ref), you avoid re-renders but lose live sync. You need to decide which model to use and, for design-system inputs, often support both so the same component works in controlled and uncontrolled usage.
Naive approach
Everything controlled: every input is value={state} and onChange={setState}. Simple and predictable, but every keystroke updates parent state and can cause unnecessary re-renders or complexity when wrapping in reusable components.
First improvement
Uncontrolled: use defaultValue (not value) and attach a ref. Read ref.current.value on submit or when you need it. The parent doesn’t re-render on each keystroke; good for simple forms where you only care about the final values.
Remaining issues
- Reusable components: A design-system
Inputmay be used controlled (parent has state) or uncontrolled (parent uses ref). Supporting both means: ifvalueis passed, treat as controlled; otherwise use internal state and optional ref. - Don’t switch mode: React warns if you switch from controlled to uncontrolled (or vice versa) for the same component instance; pick one per mount.
Production pattern
For reusable inputs: accept both value + onChange (controlled) and defaultValue (uncontrolled). Inside the component, treat “controlled” when value !== undefined and use that; otherwise use internal state. Never switch from one mode to the other for the same instance. Form libraries (React Hook Form) often use uncontrolled inputs with refs and read values on submit, which reduces re-renders.
When I use this
- Controlled: When you need live validation, formatting (e.g. phone mask), or conditional UI based on the current value.
- Uncontrolled: Simple forms where you only need values on submit; fewer re-renders and simpler parent state.
Gotchas
- value vs defaultValue: In uncontrolled mode you must not pass
value(or passundefined); usedefaultValuefor the initial value. - Warning “switching from controlled to uncontrolled”: Usually means you passed
value={undefined}orvalue={null}at some point; usevalue={value ?? ''}for strings to keep it controlled.