Expert · React concepts · topic 59 of 64 · 40 min
Lists, keys and forms
Explanation
Rendering a list with map
const dishes = [
{ id: 'd1', name: 'Masala dosa', price: 90 },
{ id: 'd2', name: 'Veg biryani', price: 160 },
{ id: 'd3', name: 'Idli', price: 40 },
];
function Menu() {
return (
<ul>
{dishes.map((dish) => (
<li key={dish.id}>
{dish.name} - Rs {dish.price}
</li>
))}
</ul>
);
}Keys
Each item in a list needs a key prop: a string or number that is unique among its siblings and stays the same for that item across renders. React uses keys to match items between renders, so it knows which item was added, removed or moved, and keeps each item's state with the right item.
Derived data: calculate, do not store twice
If a value can be calculated from props or state, calculate it while rendering. Do not keep a filteredDishes array in state next to dishes and the search text: you would have to update both every time, and sooner or later they disagree. Keep the minimum in state (the dishes and the search text) and derive the rest.
function MenuPage({ dishes }) {
const [query, setQuery] = useState('');
const [vegOnly, setVegOnly] = useState(false);
// derived on every render, never stored in state
const visible = dishes
.filter((d) => d.name.toLowerCase().includes(query.trim().toLowerCase()))
.filter((d) => !vegOnly || d.veg)
.sort((a, b) => a.price - b.price);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Search dishes" />
<p>{visible.length} dishes</p>
{/* render visible with map and key */}
</>
);
}Controlled inputs
A controlled input takes its value from state and updates that state on every change: value={name} and onChange={(e) => setName(e.target.value)}. The state is then the single source of truth, so you can check it, clear it or fill it from code at any time.
function AddressForm({ onSave }) {
const [values, setValues] = useState({ name: '', mobile: '', pin: '' });
const [errors, setErrors] = useState({});
function handleChange(e) {
const { name, value } = e.target;
setValues((v) => ({ ...v, [name]: value }));
}
function handleSubmit(e) {
e.preventDefault();
const found = validate(values); // a plain function, see the task below
setErrors(found);
if (Object.keys(found).length === 0) onSave(values);
}
return (
<form onSubmit={handleSubmit} noValidate>
<label htmlFor="pin">PIN code</label>
<input
id="pin"
name="pin"
inputMode="numeric"
value={values.pin}
onChange={handleChange}
aria-invalid={errors.pin ? true : undefined}
aria-describedby={errors.pin ? 'pin-error' : undefined}
/>
{errors.pin && <p id="pin-error" className="error">{errors.pin}</p>}
{/* the same for name and mobile */}
<button type="submit">Save address</button>
</form>
);
}The submit handler works like the one you wrote in the DOM module: stop the page reload with preventDefault, check the values, show errors, and only then send. In React 19 you can also pass a function to <form action={...}>, and React calls it with the form's FormData. Frameworks build on that, but onSubmit with preventDefault works everywhere and is still very common.
Accessible forms in React
- Every input needs a visible label. In JSX, link it with htmlFor (for is a reserved word in JavaScript), matching the input's id.
- In a component used many times, create a unique id with the useId hook instead of typing one, so two copies never share an id.
- Show errors as text next to the field, link it with aria-describedby, and set aria-invalid on the input.
- Use a real <button type="submit"> inside a <form>, so the Enter key and screen readers work.
- Choose the right input type and inputMode (email, tel, numeric) so phones show the right keyboard.
import { useId } from 'react';
function TextField({ label, ...inputProps }) {
const id = useId();
return (
<div className="field">
<label htmlFor={id}>{label}</label>
<input id={id} {...inputProps} />
</div>
);
}Watch the lesson
Lists, keys and forms · 1/6
{dishes.map((d) => <li key={d.id}>{d.name}</li>)}To show a list, map an array to elements.
Prefer a video by a teacher? Find free videos on “react rendering lists keys controlled forms validation tutorial” on YouTube ↗
In real life: The address step of a checkout
On a shopping site, the checkout shows your saved addresses as a list rendered with map and key={address.id}, so selecting one keeps working when a new address is added at the top. The "New address" form is controlled: name, mobile number and PIN code live in state, validate() checks for a 10-digit mobile number and a 6-digit PIN code on submit, and each error is shown under its field and announced by screen readers.
Practice (0/3 done)
Write the derived-data function for a menu page: visibleDishes(dishes, query, vegOnly). Each dish is { id, name, price, veg }. Return a NEW array with only the dishes whose name contains query (ignore upper/lower case and spaces around the query), keeping only veg dishes when vegOnly is true, sorted by price from low to high. Do not change the dishes array. Example: with query "biryani" and vegOnly true you get only Veg biryani.
Write validate(values) for a controlled address form. values is { name, mobile, pin }, all strings. Return an object with one message for each field that is wrong, and an empty object {} when everything is fine. Rules: name must not be empty after trimming, else "Please enter your name". mobile must be exactly 10 digits starting with 6, 7, 8 or 9, else "Enter a valid 10-digit mobile number". pin must be exactly 6 digits not starting with 0, else "Enter a valid 6-digit PIN code".
In your Vite project, build a "Saved addresses" page. Show a list of addresses from state with map and key={address.id}. Add a search box that filters the list (derived, not stored). Below it, add a controlled form for a new address (name, mobile, PIN code) that uses your validate function on submit, shows each error under its field, and adds the new address with a fresh id from crypto.randomUUID().
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.