React Text Inputs
Table of Contents + −
In the last lesson you learned about React Controlled Components, where state is the single source of truth. Now let’s apply that to the input you use most, the plain text box, so React always knows what the user typed.
🤔 Why does a text input need state?
Here’s the pain. You drop an <input> on the page, the user types, but your React code has no idea what’s in there.
- The browser keeps the typed text inside the input element, not in your component.
- So when you want to read it, send it, or show it somewhere else, you’ve got nothing to grab.
- And if you ever want to clear the box or set it from code, you can’t, because React isn’t holding the value.
So the fix is to make React the keeper of that value. We store the text in state, and the input shows whatever state holds.
🧩 The controlled text input
A controlled text input is an input whose value comes from state and whose changes go back into state. It needs three pieces working together.
This component sets up the full pattern: state, value, and onChange.
import { useState } from "react";
function NameBox() { const [name, setName] = useState("");
return ( <input value={name} onChange={(e) => setName(e.target.value)} /> );}Let’s read those three pieces one by one.
useState("")creates the state for the box and starts it empty, with an empty string"".value={name}ties the box to state, so what you see is always whatevernameholds.onChange={(e) => setName(e.target.value)}runs on every keystroke and saves the new text back into state.
So the loop is: user types, onChange updates state, React re-renders, the box shows the new state. Round and round, and React always knows the latest value.
⌨️ Reading e.target.value
Every time the user types, React fires onChange and hands your handler an event object, usually called e. The thing you actually want lives at e.target.value.
This handler is written out in full so you can see exactly where the value comes from.
function handleChange(e) { console.log(e.target.value); // whatever is currently typed setName(e.target.value); // save it into state}Here’s what each part means.
e.targetis the input element that fired the event, the actual box the user is typing in.e.target.valueis the current text inside that box, the whole string, not just the last key.- You pass that straight into your setter, like
setName(e.target.value), to keep state in sync.
So you don’t read keys one at a time. On every change React gives you the full, up-to-date text in e.target.value.
❌ Forgetting onChange vs ✅ doing it right
This is the mistake almost everyone hits once. You set value={name} but forget the onChange, so typing does nothing and the box looks frozen.
Here are both versions side by side so the difference is clear.
// ❌ Wrong - value is locked to state, but nothing ever updates state<input value={name} />// the user types... and nothing appears. The box feels broken.
// ✅ Right - onChange updates state on every keystroke, so the box responds<input value={name} onChange={(e) => setName(e.target.value)}/>Here’s why the broken one behaves that way.
value={name}forces the box to always showname, andnamenever changes without a setter.- With no
onChange, no setter ever runs, sonamestays the same and the box looks stuck. - Adding
onChangefixes it: each keystroke updates state, React re-renders, and the new text shows up.
value without onChange = a frozen box
If you set value but skip onChange, the input shows your state and refuses to change as the user types. It is not a bug in React, it is just a controlled input with no way to update its value. Always pair value with onChange.
📝 Text-like input types
A text input is not only type="text". Several input types behave the same way in React, you read them all through e.target.value. The one to watch is the number type.
This form uses four common types, and every single one is wired the same way.
<input type="text" value={name} onChange={(e) => setName(e.target.value)} /><input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /><input type="password" value={pass} onChange={(e) => setPass(e.target.value)} /><input type="number" value={age} onChange={(e) => setAge(e.target.value)} />A few things to notice about these types.
text,email, andpasswordall give you a plain string ine.target.value. Same pattern every time.- The
emailandpasswordtypes mainly change how the box looks and behaves, like hiding the password. Reading the value is identical. - The
numbertype still gives you a string, not a number. Soe.target.valuefor an age of 25 is the text"25", not the number25. - If you only need to display or store it, the string is fine. But if you need to do math with it, convert it first.
Number inputs come in as strings
Even with type="number", e.target.value is a string. If you plan to add or compare it, wrap it in Number(...) first, like Number(e.target.value). Otherwise "5" + 1 gives you "51", not 6.
💬 The placeholder attribute
A blank box can confuse people. They open your form and don’t know what goes where. The placeholder attribute fixes that with a bit of hint text.
This input shows light grey hint text until the user starts typing.
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@example.com"/>Here’s how the placeholder behaves.
- The hint text shows only while the box is empty, and it disappears the moment the user types.
- It is just a hint, not a real value. It is never part of
e.target.value, so it won’t get submitted. - Use it to show the expected format, like
you@example.comorYour full name, so people know what to enter.
💡 Putting it together: a live username preview
Now let’s build something small and real. The user types a username, and we show it back live, updating on every keystroke, no button needed.
import { useState } from "react";
function UsernamePreview() { const [username, setUsername] = useState("");
return ( <div> <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Pick a username" /> <p>Your profile: @{username}</p> </div> );}Let’s trace the whole flow as the user types.
- The user presses a key, so
onChangefires andsetUsernamesaves the new text into state. - React re-renders the component, and now
usernameholds the latest text. value={username}keeps the box in sync, and{username}in the<p>shows the same text below.- All of this happens on every keystroke, so the preview updates instantly with no submit step.
Output
(user types "alex_dev" letter by letter)
Your profile: @alex_devSo that’s the live preview in action. Because state drives both the box and the paragraph, they always show the exact same value, in real time.
✅ Best practices
A few small habits keep your text inputs clean and predictable.
- Always pair
value={...}withonChangeon a controlled input, so the box and state stay in sync. - Read the typed text from
e.target.valueinsideonChangeand save it with your setter. - Start string state at an empty string
"", notnullorundefined, so the input is controlled from the very first render. - For
type="number", remember the value is still a string, so convert withNumber(...)before doing math. - Add a clear
placeholderso people know what to type, but never rely on it as the actual value.
🧩 What You’ve Learned
- ✅ A controlled text input gets its
valuefrom state and updates state throughonChange - ✅ You read the current text from
e.target.value, which holds the full string in the box - ✅ Forgetting
onChangewhile settingvaluefreezes the box, so always pair them - ✅ The
text,email, andpasswordtypes all give you a plain string the same way - ✅ A
numberinput’s value is still a string, so useNumber(...)if you need math - ✅ The
placeholdershows hint text while the box is empty and is never part of the value - ✅ Since state drives the box, you can show the live value back to the user as they type
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
In a controlled text input, where does the input's value come from?
Why: A controlled input gets its value from state via value={...}, and onChange writes changes back into that state. State is the single source of truth.
- 2
Inside an onChange handler, how do you read what the user just typed?
Why: e.target is the input element that fired the event, so e.target.value is the full current text in that box.
- 3
You set value={name} on an input but forget onChange. What happens?
Why: value locks the box to state, and with no onChange nothing updates state. So the box shows the same value and ignores typing. Always pair value with onChange.
- 4
For an input with type number, what type is e.target.value?
Why: Even a number input gives you a string in e.target.value. Wrap it in Number(...) before doing arithmetic, or "5" + 1 becomes "51" instead of 6.
🚀 What’s Next?
Now you can handle any text-like input, read its value, and show it back live. Next we’ll look at the bigger cousin of the text box, the multi-line input you use for comments and messages.