React · State & Events

Controlled Components

A controlled input gets its value from React state; an uncontrolled one keeps its own value in the DOM. Knowing the difference explains most “why won’t my input change?” bugs.

Intermediate 5 min Lesson 13 of 42Examples target React 19 with Vite.
Before you start:Forms
0/42 in React
On this page · 5 min · Intermediate

Learning objectives#

  • Explain controlled vs uncontrolled inputs
  • Format or restrict input while typing
  • Use defaultValue and refs for uncontrolled inputs
  • Build reusable controlled components

Explanation#

Controlled: <input value={text} onChange={e => setText(e.target.value)} />. React state is the single source of truth; the input always shows state.

Because every keystroke goes through your handler, you can transform it (upper-case a PAN number, strip non-digits from a phone) or reject it.

Uncontrolled: <input defaultValue="Asha" /> — the DOM keeps the value; read it on submit (FormData or a ref). Simpler, but you cannot react to every change.

Never switch an input between controlled and uncontrolled: value={undefined} → value="text" triggers a React warning. Initialise state to "" rather than undefined.

Your own components can be controlled too: a QuantityPicker with value and onChange props works like a native input.

Syntax#

JSX Illustrative
<input value={phone} onChange={e => setPhone(e.target.value.replace(/\D/g, ''))} />   // controlled
<input name="city" defaultValue="Pune" />                                              // uncontrolled

Code example: Formatting inputs and a reusable controlled picker#

JSXCheckout.jsx Runnable
import { useState } from 'react';


function QuantityPicker({ value, onChange, min = 1, max = 10 }) {
  return (
    <span>
      <button type="button" onClick={() => onChange(Math.max(min, value - 1))} aria-label="Decrease">−</button>
      <output aria-live="polite"> {value} </output>
      <button type="button" onClick={() => onChange(Math.min(max, value + 1))} aria-label="Increase">+</button>
    </span>
  );
}


export default function Checkout() {
  const [phone, setPhone] = useState('');
  const [pan, setPan] = useState('');
  const [qty, setQty] = useState(1);


  return (
    <form>
      <label>
        Phone
        <input
          value={phone}
          onChange={e => setPhone(e.target.value.replace(/\D/g, '').slice(0, 10))}
          inputMode="numeric"
        />
      </label>
      <label>
        PAN
        <input value={pan} onChange={e => setPan(e.target.value.toUpperCase())} maxLength={10} />
      </label>
      <label>Quantity <QuantityPicker value={qty} onChange={setQty} /></label>
      <label>City (uncontrolled) <input name="city" defaultValue="Pune" /></label>
      <p>Phone: {phone || '–'} · PAN: {pan || '–'} · Qty: {qty}</p>
    </form>
  );
}
Expected output
Typing “98-765 43210 99” into Phone shows only “9876543210” (digits, max 10). Typing “abcde1234f” into PAN shows “ABCDE1234F”. The −/+ buttons change the quantity between 1 and 10. The City field keeps its own value and starts as “Pune”.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. Phone is controlled: the handler strips non-digits and limits to 10 characters before storing.
  2. PAN is upper-cased on every keystroke.
  3. QuantityPicker is a controlled custom component: value in, onChange out, clamped to min/max.
  4. City is uncontrolled: defaultValue sets the starting value and the DOM manages it.

Real-world use case#

A bank form formats card numbers in groups of four, upper-cases IFSC codes and blocks letters in amount fields — all with controlled inputs.

Common mistakes#

Input will not change

value={x} with a broken or missing onChange makes the field read-only. Check the handler updates the same state.

Switching from uncontrolled to controlled

useState() starts as undefined; React warns when it becomes a string. Start with useState("").

Using defaultValue to update later

defaultValue is only read on mount. Changing it later does nothing; use a controlled value or a key to reset.

Best practices#

  • Controlled for fields with live formatting or validation
  • Uncontrolled + FormData for simple forms
  • Initialise text state with ""
  • Give custom inputs a value/onChange API

Remember this#

Controlled = value + onChange (state is the truth).

Uncontrolled = defaultValue (the DOM is the truth).

Never start a controlled input with undefined.

Practice exercise#

Your task: Create a controlled amount input that accepts only digits and at most one decimal point, and shows the amount with ₹ below it.

Need a hint?
Accept the new value only if it matches /^\d*\.?\d*$/.
Show solution
JSX Runnable
import { useState } from 'react';


export default function AmountInput() {
  const [amount, setAmount] = useState('');
  return (
    <>
      <input
        value={amount}
        onChange={e => { if (/^\d*\.?\d*$/.test(e.target.value)) setAmount(e.target.value); }}
        inputMode="decimal"
        aria-label="Amount"
      />
      <p>₹{amount || '0'}</p>
    </>
  );
}

Mini quiz#

Controlled Components quizMultiple choice Question 1 of 3

Which input is uncontrolled?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.