React Text Inputs

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 whatever name holds.
  • 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.target is the input element that fired the event, the actual box the user is typing in.
  • e.target.value is 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 show name, and name never changes without a setter.
  • With no onChange, no setter ever runs, so name stays the same and the box looks stuck.
  • Adding onChange fixes 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, and password all give you a plain string in e.target.value. Same pattern every time.
  • The email and password types mainly change how the box looks and behaves, like hiding the password. Reading the value is identical.
  • The number type still gives you a string, not a number. So e.target.value for an age of 25 is the text "25", not the number 25.
  • 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.com or Your 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 onChange fires and setUsername saves the new text into state.
  • React re-renders the component, and now username holds 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_dev

So 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={...} with onChange on a controlled input, so the box and state stay in sync.
  • Read the typed text from e.target.value inside onChange and save it with your setter.
  • Start string state at an empty string "", not null or undefined, so the input is controlled from the very first render.
  • For type="number", remember the value is still a string, so convert with Number(...) before doing math.
  • Add a clear placeholder so people know what to type, but never rely on it as the actual value.

🧩 What You’ve Learned

  • ✅ A controlled text input gets its value from state and updates state through onChange
  • ✅ You read the current text from e.target.value, which holds the full string in the box
  • ✅ Forgetting onChange while setting value freezes the box, so always pair them
  • ✅ The text, email, and password types all give you a plain string the same way
  • ✅ A number input’s value is still a string, so use Number(...) if you need math
  • ✅ The placeholder shows 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. 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. 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. 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. 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.

React Textarea

Share & Connect