Lists, Keys, Forms
Stable keys. Controlled inputs. Common bugs.
Two things you'll do every day in React: render dynamic lists, and capture user input from forms. They both have hidden potholes. The good news: once you learn the rules, you stop falling in.
Rendering lists with map
JSX accepts arrays of elements. So to render a list, map your data:
const fruits = ["apple", "pear", "kiwi"];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);Every list element needs a key prop. React uses the key to track which item is which between renders. Without keys, React falls back to comparing by index, and the moment you add, remove, or reorder items, you get subtle, infuriating bugs.
The cardinal rule: don't use the index as key
Indexes look tempting (items.map((x, i) => ...)) but they make React think a different item moved into the same slot. If your list is static and never reorders, indexes are fine. If anything moves, breaks, or has state, you get bugs:
// Items list: ["A", "B", "C"]
// Each <Row> has its own internal state (an input value)
// User types something into Row 1 (which is "B")
// Then deletes "A" from the top of the list
// With key={i}: Row 0 is now "B" but still has the OLD state (the value the user typed)
// With key={item.id}: state moves with the item, as expected.Stable, unique IDs are the answer. Database row IDs, UUIDs, anything that follows the item around.
keydoesn't get passed to your component. If you need an ID inside, pass it as a separate prop like id={item.id}.Conditional rendering
The four patterns you'll see most:
// 1. Ternary
{isLoading ? <Spinner /> : <Content />}
// 2. && short-circuit
{user && <UserBadge user={user} />}
// 3. Variable
let body;
if (status === "loading") body = <Spinner />;
else if (status === "error") body = <ErrorPanel />;
else body = <Content />;
return body;
// 4. Early return
if (!data) return null;
return <Content data={data} />;The infamous && with 0 bug
This bug bites everyone exactly once. Then you remember it forever.
const cart = []; // empty cart
return (
<div>
{cart.length && <p>{cart.length} items</p>}
</div>
);
// You expect: nothing renders
// You get: a literal "0" appears on the pageBecause 0 && ANYTHING short-circuits to 0, and React happily renders 0 as text. Same for empty strings (though React skips those). Fix: cast to boolean.
{cart.length > 0 && <p>{cart.length} items</p>}
{Boolean(cart.length) && <p>{cart.length} items</p>}
{cart.length ? <p>{cart.length} items</p> : null}Controlled inputs
A "controlled" input is one where React owns the value. Every keystroke fires onChange, which updates state, which re-renders the input with the new value.
const [name, setName] = useState("");
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
);This is the React way. It gives you live validation, format-as-you-type, and full control. The cost: one render per keystroke. For most forms you won't notice. For very large forms, React Hook Form uses uncontrolled inputs to avoid this (later lesson).
React 19 forms with action=
React 19 brought a big shift. Native HTML <form> now accepts a function as its action prop. React handles the submit event, calls your function with FormData, manages pending state, and even works without JavaScript (progressive enhancement).
function ContactForm() {
async function submit(formData) {
const name = formData.get("name");
await saveContact({ name });
}
return (
<form action={submit}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}For most form needs, this is now the recommended starting point. You don't need useStatefor every field. You get FormData on submit. We'll cover this in detail in the React 19 chapter.
Try it: a working todo list
Stable keys, controlled input, conditional "empty" message done right (no 0 bug). Try editing items by clicking them.
Quiz
Which is the safest key for a reorderable list?
Recap
- Render lists with
.map. Every element needs a stablekey. - Avoid the index as key for anything reorderable, deletable, or stateful.
- Beware
&&with numbers. Use> 0or a ternary. - Controlled inputs: value from state, onChange sets state. The React way.
- React 19
<form action={fn}>handles submit, FormData, and pending state for you. Reach for it for new forms.