An input’s text can live in the browser or in your state. Learn both ways, what React does on each key, checkboxes, selects, number boxes, and the warnings you will meet.
In Part 6 you typed into a box whose text came from state. We called it a controlled input and said this part would explain it fully. In Part 9 you met the same word for whole components. And in Part 14 you used a ref to reach a real input on the page.
This part puts those together. It is the first part of a new stage, on forms, data and errors.
A form field is a place on a page where someone types or picks something. A text box, a checkbox and a menu are all form fields. Each one holds a value. That value can live in one of two places. It can live in the browser, or in your component’s state. This part is about that choice.
Try this first
Read this code. Don’t press Run yet.
export default function App() {
return (
<div>
<input aria-label="Box A" value="Ana" />
<input aria-label="Box B" defaultValue="Ana" />
</div>
)
}
Both boxes start with the text “Ana”. Make a guess. Which box can you type in?
Press Run. Click at the end of each box and type a letter.
Box B takes your letter. Box A doesn’t change at all. And the Console shows this warning:
You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.
“Mutable” means “can be changed”. “Read-only” means “you can look, but you can’t change it”.
value says what the box must show, always. defaultValue says what the box shows at the start. We typed “x” into both in a real browser, Chromium. Box A stayed Ana. Box B became Anax.
Two places the text can live
- Uncontrolled input. The browser keeps the text, the way it does on any web page. React sets the first value, if you give one. Then it leaves the box alone. When you need the text, you go and read it from the box.
- Controlled input. Your state keeps the text. React makes the box show exactly what your state says. A change stays only if your code puts it in state.
Box B is uncontrolled. Box A is controlled, but nobody can change its value. That is why React warned.
Uncontrolled inputs
An <input /> with no value is uncontrolled. React’s docs say it plainly: “An input like <input /> is uncontrolled.”
To give it a first value, use defaultValue. For a checkbox or a radio button, use defaultChecked. React’s docs say this JSX gives only the first value. It does not control what the value is later.
So how does your code get the text? There are two common ways.
Reading the box with a ref
Part 14 showed that a ref can hold a real tag on the page. A tag’s object in the browser is called a DOM node. An input’s DOM node has a value property: the text in the box right now.
import { useRef, useState } from 'react'
export default function App() {
const inputRef = useRef<HTMLInputElement>(null)
const [shown, setShown] = useState('nothing yet')
console.log('App renders')
function handleClick() {
if (inputRef.current !== null) {
setShown(inputRef.current.value)
}
}
return (
<div>
<input ref={inputRef} aria-label="Name" defaultValue="Ana" />
<button onClick={handleClick}>Read the box</button>
<p>You typed: {shown}</p>
</div>
)
}
App renders
App renders
App renders
App renders
Press Run. Delete “Ana”, type “Ben”, and click “Read the box”. The page shows “You typed: Ben”.
Now look at the Console. The first two lines came when the page loaded. There are two because Strict Mode renders each component twice in development (Part 2 explained this). The last two came from the click, which set state.
Typing logged nothing. We checked: while we typed six keys, App rendered 0 times. The browser changed the box by itself. React wasn’t asked to do anything.
Reading the whole form when it is sent
A ref for every box gets long. The browser has a built-in object called FormData. Give it a form, and it collects the values of the named fields. Some fields are left out, as we’ll see.
import { useState } from 'react'
export default function App() {
const [result, setResult] = useState('')
function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
setResult(JSON.stringify(Object.fromEntries(formData)))
}
return (
<form onSubmit={handleSubmit}>
<label>
Name: <input name="name" defaultValue="Ana" />
</label>
<label>
<input type="checkbox" name="news" defaultChecked={true} />
Send me news
</label>
<select name="size" aria-label="Size" defaultValue="M">
<option value="S">Small</option>
<option value="M">Medium</option>
<option value="L">Large</option>
</select>
<button type="submit">Send</button>
<p>{result}</p>
</form>
)
}
Press Run and click “Send”. The page shows {"name":"Ana","news":"on","size":"M"}.
Here is what each line does:
e.preventDefault()stops the browser from loading the page again. Part 6 explained it.new FormData(e.currentTarget)reads every named field in the form.e.currentTargetis the form, because the handler is on the form.Object.fromEntries(formData)turns it into a plain object.JSON.stringifyturns that object into text, so we can show it.
Each name becomes a key in the result. A field with no name is left out.
Look at the checkbox. When it is checked, its value is "on". MDN is a set of web guides. It says "on" is the value when a checkbox has no value of its own. Now click the checkbox so it is not checked, and click “Send” again. We tried it: the result is {"name":"Ana","size":"M"}. A checkbox that isn’t checked is not in the form data at all.
No state holds the text here. The inputs stay uncontrolled. React’s <form> page says reading with FormData “keeps the inputs uncontrolled”.
React 19 builds on this idea. You can give a form an action function instead of onSubmit. React then calls it with the FormData for you. After the Action, React’s docs say React sets the uncontrolled fields back to their first values. Part 29 covers form Actions, and what they mean for controlled fields.
Controlled inputs
A controlled input needs three things:
- a piece of state that holds the text;
value={...}, so the box always shows that state;onChange={...}, which copies each new text into state.
You saw this in Part 6. This time we log every step.
import { useState } from 'react'
export default function App() {
const [name, setName] = useState('')
console.log('App renders, name is', JSON.stringify(name))
return (
<div>
<input
aria-label="Name"
value={name}
onChange={(e) => {
console.log('onChange:', e.target.value)
setName(e.target.value)
}}
/>
<button onClick={() => setName('')}>Clear</button>
<p>Hello, {name}!</p>
</div>
)
}
App renders, name is ""
App renders, name is ""
onChange: J
App renders, name is "J"
App renders, name is "J"
onChange: Jo
App renders, name is "Jo"
App renders, name is "Jo"
App renders, name is ""
App renders, name is ""
Press Run. Type “Jo”, then click “Clear”.
The Console above shows what happens. Each key runs onChange once. Then App renders with the new text, twice because of Strict Mode. The “Clear” button sets the state to empty text. The box is emptied too, because the box always shows the state.
With a controlled box, any code that sets the state changes the box.
What React does on each key
One key press, two ways. Choose one, then press play or step through it.
Here are the controlled steps in words. Say the box shows “An”, and you press “a”.
- The browser puts “Ana” in the box. This happens first, before any of your code runs.
- React calls your
onChange.e.target.valueis the new text, “Ana”. onChangecallssetName("Ana").- React calls
Appagain. Nowvalueis"Ana". - React makes the box match
value. The box already shows “Ana”, so nothing more changes.
And the uncontrolled steps:
- The browser puts “Ana” in the box.
- There is no
onChangeand no state. React has nothing to do. Appdoesn’t render again. The box keeps “Ana”.- Later, when the form is sent, your code reads “Ana” from the box.
What if state doesn’t change?
Step 1 says the browser changes the box first. So what stops you from typing in Box A? React does, in step 5. Here is a box with an onChange that logs, but never sets state:
export default function App() {
return (
<input
aria-label="Box"
value="Ana"
onChange={(e) => console.log('onChange sees:', e.target.value)}
/>
)
}
onChange sees: Anax
Run it, click at the end of the box, and press “x”. The Console says onChange sees: Anax. So for a moment the box really held Anax. But the box still shows Ana. We checked after each key: the box was Ana again.
After the event, React compared the box with value. They didn’t match, so React put Ana back. React’s docs say the same thing. After every key, React puts the box back to the value you gave it.
This also explains the readOnly in Part 1. There, a box showed a value with value={site}. Without readOnly, React warns. A box with value and no onChange can never be changed by typing. Usually that is a bug, so React warns you. readOnly tells React that you meant it.
An everyday example
Think of a shop window with a price sign. In an uncontrolled shop, anyone can walk up and change the sign. If the owner wants to know the price, the owner goes and looks at the sign.
In a controlled shop, the price is written in the owner’s book. The sign is copied from the book. To change the price, you ask the owner. The owner writes the new price in the book, and the sign is copied again.
The exact version
In the controlled shop, nobody touches the sign directly. A real controlled box is different. The browser changes the box first, before your code runs, as the last example showed. Then React copies the state back onto the box. If your state didn’t change, the box goes back to the old text. So “the box always shows state” is true after each event, not during it.
Switching between the two
A box must stay one kind for its whole life. React’s docs say an input “cannot switch between being controlled or uncontrolled over its lifetime.”
It is easy to switch by mistake. Look at the state here:
import { useState } from 'react'
export default function App() {
const [name, setName] = useState<string>()
return (
<div>
<input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
<p>Hello, {name}!</p>
</div>
)
}
useState<string>() has no starting value, so name starts as undefined. For React, value={undefined} is the same as no value at all. So the box starts uncontrolled.
Run it and type one letter. The letter is now in state, so value is a string. The box has become controlled, and React prints this:
A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://react.dev/link/controlled-components
We checked: nothing was printed when the page loaded. The warning came right after the first key, and not again after the second.
The fix is to start with empty text: useState(''). Then value is a string from the very first render.
The other way round has its own warning. We set the state of a controlled box back to undefined. React printed “A component is changing a controlled input to be uncontrolled.”
Checkboxes and radio buttons
A checkbox has no text. It is checked or not. So a controlled checkbox uses checked, not value. In onChange, read e.target.checked.
import { useState } from 'react'
export default function App() {
const [agreed, setAgreed] = useState(false)
return (
<div>
<label>
<input
type="checkbox"
checked={agreed}
onChange={(e) => {
console.log('checked:', e.target.checked, 'value:', e.target.value)
setAgreed(e.target.checked)
}}
/>
I agree to the rules
</label>
<p>Agreed: {agreed ? 'yes' : 'no'}</p>
<button disabled={!agreed}>Continue</button>
</div>
)
}
Run it and click the checkbox. The page says “Agreed: yes”, and the “Continue” button can be pressed. disabled turns the button off, so it can’t be clicked.
The Console shows checked: true value: on. Look at value. It is "on" both times, checked or not. That’s why e.target.value is the wrong thing to read for a checkbox. We checked: after a second click, the line was checked: false value: on.
A group of radio buttons lets you pick one from a list. Picking one clears the others. Give them all the same name. To control the group, keep the picked value in state. Then give each button checked={size === s}.
import { useState } from 'react'
const sizes = ['Small', 'Medium', 'Large']
export default function App() {
const [size, setSize] = useState('Medium')
return (
<div>
{sizes.map((s) => (
<label key={s}>
<input
type="radio"
name="size"
value={s}
checked={size === s}
onChange={(e) => setSize(e.target.value)}
/>
{s}
</label>
))}
<p>You picked: {size}</p>
</div>
)
}
For a radio button, value is not what it shows. It is what the button stands for. That’s what e.target.value gives you when the button is picked. We clicked “Large”, and the page said “You picked: Large”.
Select menus
A <select> is a menu with a list of <option> tags. Control it with value on the <select> itself:
import { useState } from 'react'
export default function App() {
const [fruit, setFruit] = useState('banana')
const [colors, setColors] = useState(['red', 'blue'])
return (
<div>
<select aria-label="Fruit" value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="orange">Orange</option>
</select>
<select
aria-label="Colors"
multiple={true}
value={colors}
onChange={(e) => setColors(Array.from(e.target.selectedOptions, (option) => option.value))}
>
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<p>Fruit: {fruit}. Colors: {colors.join(', ')}.</p>
</div>
)
}
Run it. The first menu starts on “Banana”, because the state says 'banana'.
The second menu has multiple={true}. Then you can pick more than one option. Hold Ctrl (Command on a Mac) while you click. On a phone, the menu lets you pick several. Its value is an array of strings, not one string. e.target.selectedOptions is the list of picked options. Array.from(...) turns that list into an array of their values. We picked “Green” and “Blue”, and the page said “Colors: green, blue.”
In plain HTML, you mark the first choice with selected on an <option>. React’s docs say this is “not supported”. We tried it. It still picked “Banana”, but React printed this:
Use the `defaultValue` or `value` props on <select> instead of setting `selected` on <option>.
So use defaultValue or value on the <select>, as the warning says.
With FormData, a multiple menu sends one entry for each picked option. Object.fromEntries keeps only one of them. We picked “red” and “blue” and got {"colors":"blue"}. formData.getAll('colors') gave both.
Text areas
A <textarea> is a text box with many lines. In HTML, you put its first text between the tags. In React, you use value or defaultValue, the same as an <input>:
import { useState } from 'react'
export default function App() {
const [text, setText] = useState('')
return (
<div>
<textarea aria-label="Message" value={text} onChange={(e) => setText(e.target.value)} />
<p>{text.length} of 100 letters</p>
</div>
)
}
Type “Hi”, and the line under it says “2 of 100 letters”. That count is the kind of thing a controlled box makes easy. The page changes on every key, because every key sets state.
Number boxes and the empty box
<input type="number"> takes numbers. But its value is still text. That leads to a bug you met in Part 9. Here it is again, on its own:
import { useState } from 'react'
export default function App() {
const [age, setAge] = useState(20)
return (
<div>
<input aria-label="Age" type="number" value={age} onChange={(e) => setAge(Number(e.target.value))} />
<p>Next year you will be {age + 1}.</p>
</div>
)
}
Run it. Click at the end of “20”. Press Backspace twice, then type 2 and 5. Watch the box after each key.
We did exactly that in Chromium. After each key, the box showed 2, 0, 02, then 025.
The second Backspace emptied the box. Number('') is 0, so the state became the number 0. React then put a 0 in the box. Typing 2 after it gave “02”. The page still says “Next year you will be 26.” The math is right, but the box looks wrong.
The fix is to keep the text in state. Turn it into a number only when you use it:
import { useState } from 'react'
export default function App() {
const [age, setAge] = useState('20')
const ageNumber = Number(age)
return (
<div>
<input aria-label="Age" type="number" value={age} onChange={(e) => setAge(e.target.value)} />
<p>Next year you will be {ageNumber + 1}.</p>
</div>
)
}
Same keys. This time the box showed 2, an empty box, 2, then 25. React’s own <input> page does the same thing. Its age box keeps a string in state and calls Number(age) when it needs the number.
Changing what the person typed
A controlled box lets you change the text before it reaches state. Here every letter becomes a capital:
import { useState } from 'react'
export default function App() {
const [code, setCode] = useState('')
return (
<div>
<input aria-label="Code" value={code} onChange={(e) => setCode(e.target.value.toUpperCase())} />
<p>Code: {code}</p>
</div>
)
}
Type “abc” and the box shows “ABC”. That works, as long as you type at the end.
Now click between the “A” and the “B” and type “x”. The caret is the thin line in a text box that shows where the next letter goes. Where does it go?
We tried it in Chromium. Before “x”, the caret was after the first letter. After “x”, the box said AXBC, and the caret had jumped to the end. The next letter would land at the end, not where you were typing.
Here’s why. Your code made the text different from what the browser had. The browser had AxBC, and state said AXBC. So React put the new text into the box. When code put new text in the box, the browser moved the caret to the end. We saw the same in Firefox and WebKit, the engine behind Safari. React’s docs call this the caret jumping “to the beginning”. Where it lands depends on the browser. A plain controlled box doesn’t have this problem. We tried the same moves on it, and the caret stayed right after the “x”.
React’s docs list this as a bug: they say to set the state to e.target.value. So if you change the text in onChange, the caret can jump. Often it’s better to change it where you use it, like code.toUpperCase() in the place where you read it.
Refusing a key has the same problem. This box takes at most five letters. It ignores any change that would make the text longer:
import { useState } from 'react'
export default function App() {
const [code, setCode] = useState('')
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (e.target.value.length <= 5) {
setCode(e.target.value)
}
}
return (
<div>
<input aria-label="Code" value={code} onChange={handleChange} />
<p>{code.length} of 5 letters</p>
</div>
)
}
At the end of the box, a sixth letter just doesn’t appear. In the middle, it’s worse. We typed “x” after the “a” in a full box. The text stayed abcde, but the caret jumped to the end.
For length, the browser has a better tool. Write maxLength={5} on the input. Then the browser itself stops the sixth key. We typed abcdef into such a box. It held abcde, and the page said “5 of 5 letters”. Then we typed “x” after the “a”. The text stayed abcde, and the caret stayed after the “a”.
File inputs are always uncontrolled
<input type="file"> lets a person pick a file from their device. Only the person can pick the file. Code can’t. MDN says it plainly: “You cannot set the value of a file picker from a script”. The old React docs say a file input “is always an uncontrolled component”.
So don’t give it value. Read the files in onChange, from e.target.files, or from FormData when the form is sent.
import { useState } from 'react'
export default function App() {
const [info, setInfo] = useState('No file chosen yet.')
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
setInfo(file ? `You chose ${file.name}, ${file.size} bytes.` : 'No file chosen yet.')
}
return (
<div>
<input type="file" aria-label="File" onChange={handleChange} />
<p>{info}</p>
</div>
)
}
files?.[0] means “the first file, if there is a list at all”. The ?. is from Part 7. ?.[0] is the same idea, for a position in a list.
We picked a file called note.txt with 5 letters in it. The page said You chose note.txt, 5 bytes. A byte is the unit for a file’s size. Each letter of plain English text takes one byte.
We also gave a file input value="note.txt". React tried to put that text in the box, and the browser refused. In Chromium, the page stayed empty, with this error:
Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.
Which one should you use?
React’s docs give two reasons to control an input. The first: “A controlled input makes sense if you needed state anyway”. For example, the page changes as you type. The second: you want “multiple ways to adjust the input state”. For example, a button that empties or fills the box.
Here is a box that is checked as you type:
import { useState } from 'react'
export default function App() {
const [email, setEmail] = useState('')
const isValid = email.includes('@')
return (
<div>
<input aria-label="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
{email !== '' && !isValid && <p>An email needs an @.</p>}
<button disabled={!isValid}>Sign up</button>
</div>
)
}
Type “ana”. The message appears, and the button stays off. Add “@”, and the message goes away. The button can be pressed. isValid is worked out from state on every render, as Part 9 taught.
So use a controlled input when:
- the page must change as the person types, like a count or a message;
- you check the text as it is typed;
- one field changes another, like a country menu that changes the list of cities;
- a button or other code must set the text.
Use an uncontrolled input when you need the values only when the form is sent. Then give each field a name and read them with FormData. A file input is always uncontrolled.
What about speed? A controlled box renders its component on every key. We counted with Strict Mode off. Typing three letters rendered the controlled App 3 times, and the uncontrolled one 0 times. For a small component, that doesn’t matter. If a big page renders on every key, move the state down into a small component. Part 9 showed how, and React’s <input> page gives the same advice.
Controlled components and controlled inputs
Part 9 used the same two words for your own components. A component is controlled when its important information comes from props. It is uncontrolled when it keeps that information in its own state.
An <input> follows the same idea. With value, the parent decides what it shows. With defaultValue, the input keeps its own value.
Your own component can work the same way. Give it value and onChange props, and pass them on to its <input>. Then the parent owns the text. That is lifting state up, from Part 9, used for a form field.
Common mistakes
value with no onChange
You saw it in “Try this first”. The box can’t be typed in, and React warns. Pick one fix:
- you only want a starting value: use
defaultValue; - you want to control it: add
onChangethat sets the state; - you really want it fixed: add
readOnly.
A checkbox with checked and no onChange gets the same kind of warning. Its warning starts with “You provided a checked prop”. We tried it, and clicking the checkbox didn’t check it.
Starting with null or undefined
Data from a server often starts as null until it arrives:
import { useState } from 'react'
export default function App() {
const [name, setName] = useState<string | null>(null)
return <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
}
TypeScript stops this one. It says Type 'string | null' is not assignable to type 'string | number | readonly string[] | undefined'. In plain words: value can’t be null.
The playground doesn’t check types, so you can press Run. React warns when the page loads:
`value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.
Type a letter, and you also get the “changing an uncontrolled input to be controlled” warning. For React, null counts as no value, just like undefined.
The fix, from React’s docs: value={name ?? ''}. The ?? operator means “use the right side if the left side is null or undefined“. So the box always gets a string. We checked: then nothing was printed.
Expecting defaultValue to update
import { useState } from 'react'
export default function App() {
const [name, setName] = useState('Ana')
return (
<div>
<input aria-label="Name" defaultValue={name} />
<button onClick={() => setName('Ben')}>Change to Ben</button>
<p>State: {name}</p>
</div>
)
}
Run it and click the button. The paragraph says “State: Ben”. The box still says “Ana”. We checked in Chromium, with and without typing in the box first. The box never changed.
defaultValue is only the first value. To change the box from code, control it with value and onChange.
React does change the box’s default to “Ben”. You can’t see it until the form is reset. We put the box in a form with a <button type="reset">. After a click on it, the box showed “Ben”. Part 29 comes back to this.
To start over with a new first value, give the input a new key. Part 4 showed that when the key changes, React treats it as a new one. We tried <input key={name} defaultValue={name} />. We typed in the box, then clicked the button. The box showed “Ben”.
Using both value and defaultValue
import { useState } from 'react'
export default function App() {
const [name, setName] = useState('Ana')
return <input aria-label="Name" type="text" value={name} defaultValue="Ben" onChange={(e) => setName(e.target.value)} />
}
Run it. The box shows “Ana”, so value wins. React warns:
App contains an input of type text with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://react.dev/link/controlled-components
Pick one. Remove defaultValue, and put the starting text in useState('Ben') instead.
Putting text between <textarea> tags
export default function App() {
return <textarea aria-label="Message">Hello</textarea>
}
It shows “Hello”, but React warns:
Use the `defaultValue` or `value` props instead of setting children on <textarea>.
Write <textarea defaultValue="Hello" /> instead.
Practice
Press Edit on the examples above and try these.
- In “Try this first”, make Box A something you can type in. Find two different ways.
- Run the controlled example with the logs again. Type “Ana”, and don’t click “Clear”. How many lines does the Console show?
- In the
FormDataexample, give the checkboxvalue="yes". Click “Send”. Then click the checkbox so it is not checked, and click “Send” again. What does the page show each time? - Change the five-letter code box so that it takes only the numbers 0 to 9, and still at most five. A letter should not get into the box.
Answers
- Way one: change
value="Ana"todefaultValue="Ana". The box becomes uncontrolled. Way two: keepvalue, but take it from state and addonChange:
import { useState } from 'react'
export default function App() {
const [text, setText] = useState('Ana')
return (
<div>
<input aria-label="Box A" value={text} onChange={(e) => setText(e.target.value)} />
<input aria-label="Box B" defaultValue="Ana" />
</div>
)
}
Either way, the warning is gone.
-
11 lines. On load,
Apprenders twice because of Strict Mode: 2 lines. Each of the 3 letters then logs 1onChangeline and 2 render lines: 9 more. -
Checked, the page shows
{"name":"Ana","news":"yes","size":"M"}. The checkbox sends its ownvaluenow, not"on". Not checked, it shows{"name":"Ana","size":"M"}, the same as before. -
Use
maxLength={5}for the length, as above. For the numbers, test the new text before you keep it./^[0-9]*$/is a regular expression: a small pattern for text. This one means “only the numbers 0 to 9, from start to end”..test(text)givestrueif the text fits the pattern.
import { useState } from 'react'
export default function App() {
const [code, setCode] = useState('')
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
if (/^[0-9]*$/.test(e.target.value)) {
setCode(e.target.value)
}
}
return (
<div>
<input aria-label="Code" maxLength={5} value={code} onChange={handleChange} />
<p>Code: {code}</p>
</div>
)
}
We typed 1, a, 2, 3, 4, 5. The box showed “1”, then “1” again, then “12”, “123”, “1234” and “12345”. This is a trade-off you accept. A letter refused in the middle of the box makes the caret jump, as with the five-letter box. We typed “a” after the “1” in “123” in Chromium, and the caret went to the end.
Interview questions
Try to answer each one out loud before you open the answer.
What is the difference between a controlled and an uncontrolled input?
In an uncontrolled input, the browser keeps the value. You may give a first value with defaultValue, and read the value later with a ref or FormData. In a controlled input, state keeps the value. You pass value and an onChange that updates the state.
A strong answer adds that the browser still changes the box first. React then puts the state’s value back if they differ. That is why a controlled box with no state update can’t be typed in.
Why can’t I type in my input?
It probably has value but no onChange, or an onChange that doesn’t update the state. React makes the box show value after each key. If value never changes, the box never changes. Use defaultValue, write a real onChange, or add readOnly.
What does “A component is changing an uncontrolled input to be controlled” mean?
The input’s value was undefined (or null), and then became a string. React treats a missing value as uncontrolled, so the input switched kinds. That usually means state started as undefined or null, like data still coming from a server. Fix it with useState(''), or with value={name ?? ''}. The reverse, a string becoming undefined, gives “changing a controlled input to be uncontrolled”.
How do you read the values of an uncontrolled form?
Give each field a name. In onSubmit, call e.preventDefault(), then new FormData(e.currentTarget). Read single fields with formData.get('name'). Use Object.fromEntries(formData) for a plain object. For one field, a ref and ref.current.value also work.
A strong answer knows the special cases. A checkbox that isn’t checked is left out. A checked one with no value sends "on". A <select multiple> sends one entry for each picked option. Object.fromEntries keeps only one of them, so you need getAll. It also says that React 19 can pass the FormData to a form’s action function.
When should you choose a controlled input?
Choose it when you need the value while the person types. You might show it, check it or change other fields with it. Or a button might need to set it. React’s docs say a controlled input “makes sense if you needed state anyway”. When you need the values only once, at submit, uncontrolled fields with FormData need less code.
A strong answer adds that a controlled box renders its component on every key. That is usually fine. If it isn’t, move the state into a smaller component.
Why does the caret jump in my input?
Your onChange sets the state to something other than e.target.value, like a copy in capital letters. Then the state and the box differ, so React writes the new text into the box. When code sets the text, the browser moves the caret. In our tests it jumped to the end. React’s docs name two more causes. One is setting the state later, in a timer. The other is a box that React adds again on every render. Set state to e.target.value right away, and change the text where you use it.
Sources
<input>, react.dev:value,checked,defaultValue,defaultChecked, and the rule that an input “cannot switch between being controlled or uncontrolled over its lifetime”. Also “An input like<input />is uncontrolled”, the first value only, reading a form withFormData, and “A controlled input makes sense if you needed state anyway”. Also “multiple ways to adjust the input state”, putting the box back after every key, the age box kept as a string, a render on every key, the caret jumping, and?? ''.<select>, react.dev:valueanddefaultValueon the select,multiplewith an array,selectedOptions, noselectedon<option>, and oneFormDataentry per picked option.<textarea>, react.dev:valueanddefaultValue, and no children.<form>, react.dev:new FormData(e.target)“keeps the inputs uncontrolled”, and theactionprop, which gets the form data.- React v19, React blog: forms accept functions as
action. - Sharing State Between Components, react.dev: controlled and uncontrolled components.
- useRef, react.dev: reading a DOM node through a ref.
- Uncontrolled Components, the old React docs: “
<input type="file" />is always an uncontrolled component because its value can only be set by a user”. - MDN:
<input type="checkbox">(the value isonwhen none is given; a checkbox that isn’t checked is not sent),<input type="radio">(one group shares onename),<select>(Ctrl, or Command on a Mac, to pick more than one),<input type="file">(“You cannot set the value of a file picker from a script”),<input type="number">,FormDataandFormData.getAll(). - The exact text of each warning, and when each one is printed, comes from
react/checks/output/part28-lab.txt(React 19.3.0 development build). - Every warning, render count, box value and caret position above comes from running React 19.3.0 for this post, in jsdom and in headless Chromium 151 with a real keyboard. The caret check was also run in Firefox 153 and WebKit 26.5.
- This part follows the Controlled vs Uncontrolled kata in react-katas.