Skip to content

Expert · React concepts · topic 59 of 64 · 40 min

Lists, keys and forms

Explanation

Turn an array into a list of elements with map, and give each item a stable key, such as its id, so React can track it. Work out filtered and sorted lists from state while rendering instead of storing them twice. For forms, keep each input's value in state (a controlled input), check the values on submit, and link every label to its input with htmlFor.

Rendering a list with map

javascript
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>
  );
}
map turns each dish into an <li>; key={dish.id} identifies each one (React code, runs in a React project)

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.

Think of it like this: Keys are like roll numbers in a class. If students change benches, the teacher still knows who is who by roll number. Using the array index as a key is like calling students by bench position: when one student leaves, everyone after them gets the wrong name.
Careful: Do not use the array index as a key when the list can be filtered, sorted, or have items added or removed: React may keep typed text or a ticked checkbox on the wrong row. Never use Math.random() as a key either: a new key on every render makes React throw away and rebuild every item. Use an id from your data, or create one with crypto.randomUUID() when the item is created.

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.

javascript
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 */}
    </>
  );
}
visible is worked out from dishes, query and vegOnly every time (React code, runs in a React project)
Tip: sort() changes the array it is called on. Sort a copy: filter already gives a new array, or use [...list].sort(...) or list.toSorted(...). Sorting a props or state array in place would mutate it.

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.

javascript
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>
  );
}
One state object for the whole form; [name]: value updates the field whose name attribute matches (React code, runs in a React project)

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.

Careful: An input with value but no onChange cannot be typed into, and React prints a warning. Either add onChange, or use defaultValue for an uncontrolled input that the browser manages.

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.
javascript
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>
  );
}
useId gives every copy of TextField its own id, so each label points to the right input (React code, runs in a React project)

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)

Task 1 · Code · Javascript

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.

Task 2 · Code · Javascript

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".

Task 3 · Practice

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

Question 1

1.Why does React need a key on each item of a list?

Question 2

2.You render a list of orders from a server, and the user can delete orders. What is the best key?

Question 3

3.A component keeps dishes, query and filteredDishes in three pieces of state. What is the problem?

Question 4

4.What makes an input "controlled" in React?

Question 5

5.How do you link a label to an input with id="mobile" in JSX?

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.

← Previous: State and events