React 26 ⚛️ Handling Textareas and Select Dropdowns
The previous chapter covered controlled inputs for the basic field types: text, checkbox, radio, and the general pattern of binding value to state and onChange to an updater. This chapter focuses on two elements that behave differently in React than they do in plain HTML: <textarea> and <select>. In HTML, a textarea’s value is its text content, between the opening and closing tags. In React, the value is a prop. In HTML, a select’s selected option is marked with the selected attribute. In React, the selected option is determined by the value on the <select> element. These differences are small but they trip up developers who are used to the HTML way.
This chapter covers both elements in full: the controlled pattern, the props that differ from HTML, handling multiple selections, working with dynamic options, and the common mistakes that produce read-only fields or selections that do not update.
Key point: In React, <textarea> uses a value prop, not children. <select> uses a value prop on the select element, not selected on the option. Both are controlled the same way as text inputs: bind the value to state and update the state in onChange.
Why these elements differ in React
The children problem. In HTML, a textarea’s value is its children: <textarea>Hello</textarea>. In React, using children for the value produces a warning, and the value cannot be updated from state. React treats <textarea> like <input>: the value is a prop. This is consistent with the controlled component pattern and avoids the inconsistency of having some elements take their value as props and others as children.
The selected problem. In HTML, the selected option in a select is marked with the selected attribute: <option selected>Apple</option>. In React, the selected attribute on an option is ignored. Instead, the <select> element receives a value prop that matches one of its options. React then sets the selected state internally. This is again consistent with the controlled pattern and makes it possible to change the selection from state.
The multiple problem. A multi-select in HTML allows multiple options to be selected. In React, the same element uses a value prop that is an array of the selected option values. The onChange handler receives an event whose target.selectedOptions contains the selected options. The handler must extract the values from selectedOptions and update the array in state.
The dynamic options problem. Options are often generated from an array, using map. The same pattern used for any list applies: each option needs a key, and the value attribute is the option’s value. When the options change, the select re-renders and the selected option is determined by the value prop.
The object value problem. HTML option values are strings. To bind an option to an object or a number, the value must be converted. The common pattern is to store the ID as the option value and look up the object from the ID in the change handler.
a. Controlled textareas
A controlled textarea uses the value prop and an onChange handler, exactly like a text input.
function BioForm() {
const [bio, setBio] = useState('');
return (
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
rows={4}
placeholder="Tell us about yourself"
/>
);
}
The value prop sets the textarea’s content. The onChange handler reads e.target.value—the current text—and updates the state. React re-renders, the textarea receives the new value, and the displayed text matches the state.
The common mistake is to use children instead of value:
// Wrong in React
<textarea>{bio}</textarea>
React will warn that a textarea with children should not also have a value prop, and the textarea will not be updatable from state. The correct form is always value={bio}.
The textarea supports the same modifiers as text inputs in form libraries, but in plain React, the modifiers are just part of the handler. To update on blur instead of on every keystroke, use onBlur instead of onChange:
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
onBlur={(e) => saveToServer(e.target.value)}
/>
The onChange keeps the state in sync on every keystroke. The onBlur runs when the field loses focus, which is a good place for expensive operations like saving or validating.
b. Controlled select dropdowns
A controlled select uses the value prop on the <select> element and onChange to update the state.
function FruitPicker() {
const [fruit, setFruit] = useState('apple');
return (
<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</select>
);
}
The value prop on the <select> matches one of the option values. React selects the matching option. The onChange handler reads e.target.value—the value of the selected option—and updates the state.
The common mistake is to use selected on an option:
// Wrong in React
<select>
<option value="apple" selected>Apple</option>
<option value="banana">Banana</option>
</select>
React ignores the selected attribute and controls the selection through the value prop on the select. Using selected produces a warning, and the selection does not update from state.
A placeholder option is a common pattern. It has an empty value and is disabled, so the user must select a real option:
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="" disabled>Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="mx">Mexico</option>
</select>
The value="" on the placeholder matches the initial state. When the user selects a real option, the state updates. The placeholder is disabled so it cannot be re-selected.
Options are often generated from an array:
function CountryPicker({ countries }) {
const [country, setCountry] = useState('');
return (
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="" disabled>Select a country</option>
{countries.map((c) => (
<option key={c.code} value={c.code}>
{c.name}
</option>
))}
</select>
);
}
Each option has a key for React’s list reconciliation and a value that is stored in state when selected. The value is usually the ID or code, not the display name, so that the state is stable when the display name changes.
c. Multi-select and object values
A multi-select uses a value prop that is an array of the selected values and an onChange handler that extracts the selected options.
function TagPicker({ availableTags }) {
const [selected, setSelected] = useState([]);
const handleChange = (e) => {
const values = Array.from(e.target.selectedOptions, (opt) => opt.value);
setSelected(values);
};
return (
<select multiple value={selected} onChange={handleChange}>
{availableTags.map((tag) => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
);
}
The multiple attribute allows multiple selections. The value prop is an array. The onChange handler uses e.target.selectedOptions, which is an HTMLCollection of the selected <option> elements. Array.from converts it to an array, and the mapping extracts the value from each option.
The selectedOptions collection is the reliable source of the selected values. Reading e.target.value on a multi-select returns only the first selected option in most browsers, which is rarely what is wanted.
To bind an option to an object rather than a string, store the ID as the value and look up the object from the ID:
function UserPicker({ users }) {
const [userId, setUserId] = useState('');
const selectedUser = users.find((u) => u.id === userId);
return (
<>
<select value={userId} onChange={(e) => setUserId(e.target.value)}>
<option value="" disabled>Select a user</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.name}
</option>
))}
</select>
{selectedUser && <p>Selected: {selectedUser.name}</p>}
</>
);
}
The value on the option is user.id, which is a string or number. The state stores the ID. The selected user is looked up from the array. This pattern keeps the state primitive and avoids the problem of comparing objects by reference.
For a multi-select with object values, the state is an array of IDs:
const [userIds, setUserIds] = useState([]);
const handleChange = (e) => {
const values = Array.from(e.target.selectedOptions, (opt) => opt.value);
setUserIds(values);
};
const selectedUsers = users.filter((u) => userIds.includes(u.id));
The state is an array of IDs. The selected users are computed from the array. This is the same pattern as the single-select, extended to multiple values.
Complete Example Session
// ============================================
// PART 1: BASIC CONTROLLED TEXTAREA
// ============================================
import { useState } from 'react';
function BioForm() {
const [bio, setBio] = useState('');
return (
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
rows={4}
/>
);
}
// ============================================
// PART 2: TEXTAREA WITH CHARACTER COUNT
// ============================================
function BioForm() {
const [bio, setBio] = useState('');
const maxLength = 200;
return (
<div>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
maxLength={maxLength}
rows={4}
/>
<p>{bio.length}/{maxLength}</p>
</div>
);
}
// ============================================
// PART 3: TEXTAREA WITH ONBLUR
// ============================================
function BioForm() {
const [bio, setBio] = useState('');
return (
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
onBlur={(e) => saveToServer(e.target.value)}
rows={4}
/>
);
}
// ============================================
// PART 4: BASIC CONTROLLED SELECT
// ============================================
function FruitPicker() {
const [fruit, setFruit] = useState('apple');
return (
<select value={fruit} onChange={(e) => setFruit(e.target.value)}>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</select>
);
}
// ============================================
// PART 5: SELECT WITH PLACEHOLDER
// ============================================
function CountryPicker() {
const [country, setCountry] = useState('');
return (
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="" disabled>Select a country</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="mx">Mexico</option>
</select>
);
}
// ============================================
// PART 6: DYNAMIC OPTIONS
// ============================================
function CountryPicker({ countries }) {
const [country, setCountry] = useState('');
return (
<select value={country} onChange={(e) => setCountry(e.target.value)}>
<option value="" disabled>Select a country</option>
{countries.map((c) => (
<option key={c.code} value={c.code}>{c.name}</option>
))}
</select>
);
}
// ============================================
// PART 7: MULTI-SELECT
// ============================================
function TagPicker({ availableTags }) {
const [selected, setSelected] = useState([]);
const handleChange = (e) => {
const values = Array.from(e.target.selectedOptions, (opt) => opt.value);
setSelected(values);
};
return (
<select multiple value={selected} onChange={handleChange}>
{availableTags.map((tag) => (
<option key={tag} value={tag}>{tag}</option>
))}
</select>
);
}
// ============================================
// PART 8: OBJECT VALUES WITH LOOKUP
// ============================================
function UserPicker({ users }) {
const [userId, setUserId] = useState('');
const selectedUser = users.find((u) => u.id === userId);
return (
<>
<select value={userId} onChange={(e) => setUserId(e.target.value)}>
<option value="" disabled>Select a user</option>
{users.map((user) => (
<option key={user.id} value={user.id}>{user.name}</option>
))}
</select>
{selectedUser && <p>Selected: {selectedUser.name}</p>}
</>
);
}
// ============================================
// PART 9: MULTI-SELECT WITH OBJECT LOOKUP
// ============================================
function UserMultiPicker({ users }) {
const [userIds, setUserIds] = useState([]);
const handleChange = (e) => {
const values = Array.from(e.target.selectedOptions, (opt) => opt.value);
setUserIds(values);
};
const selectedUsers = users.filter((u) => userIds.includes(u.id));
return (
<div>
<select multiple value={userIds} onChange={handleChange}>
{users.map((user) => (
<option key={user.id} value={user.id}>{user.name}</option>
))}
</select>
<p>Selected: {selectedUsers.map((u) => u.name).join(', ')}</p>
</div>
);
}
// ============================================
// PART 10: FORM WITH TEXTAREA AND SELECT
// ============================================
function FeedbackForm() {
const [form, setForm] = useState({
category: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
<select name="category" value={form.category} onChange={handleChange}>
<option value="" disabled>Select a category</option>
<option value="bug">Bug report</option>
<option value="feature">Feature request</option>
<option value="other">Other</option>
</select>
<textarea
name="message"
value={form.message}
onChange={handleChange}
rows={4}
placeholder="Describe your feedback"
/>
<button type="submit">Submit</button>
</form>
);
}
The ten parts covered basic controlled textareas, character count, onBlur, basic controlled selects, placeholder options, dynamic options, multi-select, object values with lookup, multi-select with object lookup, and a combined form.
Quick Reference
Textarea Props
| Prop | Purpose |
|---|---|
value | Current text (controlled) |
onChange | Update state on input |
onBlur | Run on focus loss |
rows | Visible height |
maxLength | Maximum characters |
placeholder | Hint text |
Select Props
| Prop | Purpose |
|---|---|
value | Selected value or array |
onChange | Update state on selection |
multiple | Allow multiple selections |
name | Form field name |
Select Patterns
| Pattern | Value Type |
|---|---|
| Single select | String |
| Multi-select | Array of strings |
| Object lookup | ID (string or number) |
| Placeholder | Empty string |
Multi-Select Handling
| Step | Code |
|---|---|
| Get options | e.target.selectedOptions |
| Convert to array | Array.from(...) |
| Extract values | (opt) => opt.value |
| Update state | setSelected(values) |
Common Mistakes
| Mistake | Correct Form |
|---|---|
<textarea>{value}</textarea> | <textarea value={value}> |
<option selected> | <select value={value}> |
e.target.value on multi-select | e.target.selectedOptions |
| Object as option value | ID as value, lookup object |
Best Practices
✅ Do This:
// Use value prop on textarea
<textarea value={bio} onChange={(e) => setBio(e.target.value)} /> // ✅
// Use value prop on select
<select value={fruit} onChange={(e) => setFruit(e.target.value)}> // ✅
// Use a disabled placeholder option
<option value="" disabled>Select</option> // ✅
// Add keys to dynamic options
{countries.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)} // ✅
// Use selectedOptions for multi-select
Array.from(e.target.selectedOptions, (opt) => opt.value) // ✅
// Store IDs, not objects, in state
const [userId, setUserId] = useState(''); // ✅
// Look up the object from the ID
const user = users.find((u) => u.id === userId); // ✅
❌ Don’t Do This:
// Don't use children for textarea value
<textarea>{bio}</textarea> // ❌
// Don't use selected on options
<option selected>Apple</option> // ❌
// Don't read e.target.value for multi-select
const value = e.target.value; // ❌
// Don't store objects in select state
const [user, setUser] = useState(null); // ⚠️
// Don't forget keys on dynamic options
{countries.map((c) => <option value={c.code}>{c.name}</option>)} // ❌
// Don't skip the placeholder value match
<option value="" disabled>Select</option>
// state must start as '' // ❌ if state starts 'us'
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Textarea is read-only | Used children, not value | Use value prop |
| Select does not update | Used selected on option | Use value on select |
| Multi-select returns one value | Read e.target.value | Use selectedOptions |
| Placeholder does not show | State does not match empty value | Start state as '' |
| Selected option not matching | Value type mismatch | Ensure option values match state type |
| Options reorder unexpectedly | Missing keys | Add key to each option |
Real-World Examples
1. Comment Box
<textarea value={comment} onChange={(e) => setComment(e.target.value)} rows={4} />
2. Feedback Form
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="" disabled>Select a category</option>
<option value="bug">Bug</option>
<option value="feature">Feature</option>
</select>
3. Country Picker
{countries.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
4. Tag Multi-Select
<select multiple value={tags} onChange={handleChange}>
{allTags.map((t) => <option key={t} value={t}>{t}</option>)}
</select>
5. User Assignment
<select value={userId} onChange={(e) => setUserId(e.target.value)}>
<option value="" disabled>Assign to…</option>
{users.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
</select>
6. Priority Selector
<select value={priority} onChange={(e) => setPriority(e.target.value)}>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
7. Bio with Counter
<textarea value={bio} onChange={(e) => setBio(e.target.value)} maxLength={200} />
<p>{bio.length}/200</p>
8. Multi-Select Roles
const [roles, setRoles] = useState([]);
const handleChange = (e) => {
setRoles(Array.from(e.target.selectedOptions, (o) => o.value));
};
9. Dependent Selects
<select value={country} onChange={(e) => setCountry(e.target.value)}>
{countries.map((c) => <option key={c.code} value={c.code}>{c.name}</option>)}
</select>
<select value={city} onChange={(e) => setCity(e.target.value)}>
{cities.filter((c) => c.country === country).map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
10. Combined Form Handler
const handleChange = (e) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
Visual
Textarea Value Flow
┌─────────────────────────────────────────────────────────────┐
│ TEXTAREA CONTROLLED FLOW │
│ │
│ State: bio = '' │
│ │ │
│ ▼ │
│ <textarea value={bio} onChange={...} /> │
│ │ │
│ ▼ │
│ User types "Hello" │
│ │ │
│ ▼ │
│ onChange fires │
│ │ │
│ ▼ │
│ setBio('Hello') │
│ │ │
│ ▼ │
│ React re-renders with value="Hello" │
│ │ │
│ ▼ │
│ Textarea displays "Hello" │
│ │
│ Value is a prop, not children. │
│ │
└─────────────────────────────────────────────────────────────┘
Select Value Flow
┌─────────────────────────────────────────────────────────────┐
│ SELECT CONTROLLED FLOW │
│ │
│ State: fruit = 'apple' │
│ │ │
│ ▼ │
│ <select value={fruit} onChange={...}> │
│ <option value="apple">Apple</option> │
│ <option value="banana">Banana</option> │
│ </select> │
│ │ │
│ ▼ │
│ React selects the option whose value matches 'apple' │
│ │ │
│ ▼ │
│ User selects Banana │
│ │ │
│ ▼ │
│ setFruit('banana') │
│ │ │
│ ▼ │
│ React re-renders with value="banana" │
│ │ │
│ ▼ │
│ Banana is selected │
│ │
│ Selection is determined by the value prop, not `selected`. │
│ │
└─────────────────────────────────────────────────────────────┘
Multi-Select Handling
┌─────────────────────────────────────────────────────────────┐
│ MULTI-SELECT EVENT │
│ │
│ <select multiple value={selected} onChange={handleChange}> │
│ │
│ User selects Apple and Cherry. │
│ │
│ e.target.selectedOptions: │
│ HTMLCollection [<option value="apple">, │
│ <option value="cherry">] │
│ │
│ Array.from(...) → [<option>, <option>] │
│ │
│ .map(opt => opt.value) → ['apple', 'cherry'] │
│ │
│ setSelected(['apple', 'cherry']) │
│ │
│ e.target.value returns only the first selected value. │
│ Use selectedOptions for all of them. │
│ │
└─────────────────────────────────────────────────────────────┘
HTML vs React
┌─────────────────────────────────────────────────────────────┐
│ HTML │
│ │
│ <textarea>Hello</textarea> │
│ <select> │
│ <option selected>Apple</option> │
│ </select> │
│ │
│ Value is children; selection is `selected`. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ REACT │
│ │
│ <textarea value="Hello" onChange={...} /> │
│ <select value="apple" onChange={...}> │
│ <option value="apple">Apple</option> │
│ </select> │
│ │
│ Value is a prop; selection is the `value` on the select. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Textarea value | value prop, not children |
| Select value | value on select, not selected on option |
| Placeholder option | value="" disabled |
| Dynamic options | map with key and value |
| Multi-select | multiple attribute, value is array |
| Multi-select handler | e.target.selectedOptions |
| Object values | Store ID, look up object |
| Combined form | Generic handler with name |
Key takeaways:
<textarea>uses avalueprop in React. Children are not the value. This is consistent with the controlled input pattern and makes the textarea updatable from state.<select>uses avalueprop on the select element. Theselectedattribute on options is ignored by React. The value of the select determines which option is selected.- A placeholder option has an empty value and is disabled. The state must be initialized to
''to match the placeholder. When the user selects a real option, the state updates to that option’s value. - Dynamic options are generated with
map. Each option needs akeyfor React’s reconciliation and avaluefor the state. The value is usually an ID or code, not the display text. - Multi-select uses an array for the value. The
onChangehandler readse.target.selectedOptionsand extracts the values. Readinge.target.valueon a multi-select returns only the first selected value. - Object values are stored as IDs. Storing an object in state and comparing by reference is error-prone. Store the ID, and look up the object from the array when needed.
- A generic form handler works for both textareas and selects. The handler reads
e.target.nameande.target.valueand updates the corresponding field in a state object. This pattern scales to any number of fields.
Remember: Textareas and selects are controlled the same way as text inputs. The value is a prop, not children or an attribute. The selection is determined by the value prop on the select, not by the selected attribute on an option. Multi-selects use arrays and selectedOptions. Object values are stored as IDs and looked up. These patterns are small but they matter: they are the difference between a form that works and a form that produces warnings and does not update. Learn them once, and every form in every React application will follow the same pattern.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!