Skip to content
elephantoo

Lists & keys

Lesson 8 of 30 14 min read

Render arrays with map, choose good keys, filter and sort — plus a first look at effects.


Most UIs display collections: lessons in a course, products in a shop, messages in a chat. In React you turn arrays of data into arrays of elements with map, and give each element a key so React can keep track of it. At the end of this lesson you'll also take a first look at effects, which you'll study in depth in the next module.

Rendering lists with map#

JSX can render arrays, so the idiomatic way to show a list is to map data to elements:

JSX
const lessons = [
  { id: "intro", title: "What is React?", minutes: 12 },
  { id: "props", title: "Components & props", minutes: 15 },
  { id: "state", title: "State with useState", minutes: 18 },
];

export default function LessonList() {
  return (
    <ol>
      {lessons.map((lesson) => (
        <li key={lesson.id}>
          {lesson.title} <small>({lesson.minutes} min)</small>
        </li>
      ))}
    </ol>
  );
}

lessons.map(...) returns an array of <li> elements, which React renders in order. Remember: an arrow function with parentheses (lesson) => ( ... ) returns the JSX automatically; with braces you need an explicit return.

Extracting an item component

When each item gets complex, give it its own component — and put the key on the component in the map, not inside it:

JSX
function LessonRow({ lesson, done }) {
  return (
    <li className={done ? "done" : undefined}>
      {done ? "✅" : "⬜"} {lesson.title}
    </li>
  );
}

function Syllabus({ lessons, completed }) {
  return (
    <ul>
      {lessons.map((lesson) => (
        <LessonRow key={lesson.id} lesson={lesson} done={completed.includes(lesson.id)} />
      ))}
    </ul>
  );
}

key is special: React uses it, and it is not passed to your component as a prop.

Keys#

Every item in a list needs a key that is:

  • Unique among its siblings (keys only need to be unique within one list), and
  • Stable — the same item gets the same key on every render.
KeyVerdict
key={lesson.id} (database id, slug, UUID)✅ Best
key={item.email} (naturally unique field)✅ Fine
key={index}⚠️ Only for static lists that never reorder, filter or insert
key={Math.random()} / crypto.randomUUID() in render❌ New key every render: destroys and recreates every item

Forget the key and React warns in the console: Each child in a list should have a unique "key" prop.

Why index keys cause bugs

Keys tell React which item is which. With index keys, if you insert an item at the top, every item's key shifts — React thinks item 0 changed rather than that a new item appeared. Any state inside the items (an input's text, a checkbox, an animation) stays attached to the wrong row:

JSX
import { useState } from "react";

function Row({ label }) {
  const [note, setNote] = useState(""); // state that lives inside each row
  return (
    <li>
      {label} <input value={note} onChange={(e) => setNote(e.target.value)} placeholder="note" />
    </li>
  );
}

export default function KeyDemo() {
  const [people, setPeople] = useState([
    { id: 1, name: "Ada" },
    { id: 2, name: "Alan" },
  ]);

  function addToTop() {
    setPeople([{ id: Date.now(), name: "Grace" }, ...people]);
  }

  return (
    <>
      <button onClick={addToTop}>Add Grace to the top</button>
      <ul>
        {people.map((p, index) => (
          <Row key={index} label={p.name} /> // try changing to key={p.id}
        ))}
      </ul>
    </>
  );
}

Type a note next to "Ada", then click the button. With key={index}, your note jumps to "Grace" (now at index 0). With key={p.id}, it stays with Ada. If your data has no ids, generate them when you create the data (e.g. crypto.randomUUID() when adding an item), never during render.

Fragments with keys

To render several elements per item without a wrapper, use Fragment with a key:

JSX
import { Fragment } from "react";

function Glossary({ terms }) {
  return (
    <dl>
      {terms.map((t) => (
        <Fragment key={t.term}>
          <dt>{t.term}</dt>
          <dd>{t.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

Filtering and sorting before rendering#

Prepare the data with array methods, then map:

JSX
import { useState } from "react";

const courses = [
  { slug: "javascript", title: "JavaScript", level: "beginner", lessons: 34 },
  { slug: "react", title: "React", level: "intermediate", lessons: 30 },
  { slug: "linux", title: "Linux", level: "beginner", lessons: 25 },
];

export default function CourseBrowser() {
  const [query, setQuery] = useState("");
  const [sortBy, setSortBy] = useState("title");

  const visible = courses
    .filter((c) => c.title.toLowerCase().includes(query.toLowerCase()))
    .toSorted((a, b) => (sortBy === "title" ? a.title.localeCompare(b.title) : b.lessons - a.lessons));

  return (
    <section>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search courses" />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="title">A–Z</option>
        <option value="lessons">Most lessons</option>
      </select>

      {visible.length === 0 ? (
        <p>No courses match “{query}”.</p>
      ) : (
        <ul>
          {visible.map((c) => (
            <li key={c.slug}>
              {c.title} — {c.lessons} lessons ({c.level})
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}

Notice that visible is derived during render, not stored in state — it's always in sync with query and sortBy. Use toSorted (or [...arr].sort()) so you never mutate the original array.

Nested lists#

JSX
function Curriculum({ modules }) {
  return (
    <div>
      {modules.map((m) => (
        <section key={m.title}>
          <h3>{m.title}</h3>
          <ul>
            {m.lessons.map((l) => (
              <li key={l.slug}>{l.title}</li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}

Each inner list has its own key scope, so two modules could both contain a lesson keyed "intro" without a clash.

A first look at effects#

Rendering must be pure: compute JSX from props and state, nothing else. But sometimes a component needs to synchronise with something outside React — the page title, a timer, a network request, a browser API. That's what useEffect is for. It runs your code after React has updated the screen:

JSX
import { useEffect, useState } from "react";

export default function TodoCounter() {
  const [todos, setTodos] = useState(["Learn lists", "Learn keys"]);
  const remaining = todos.length;

  useEffect(() => {
    document.title = `${remaining} todos left · Elephantoo`;
  }, [remaining]); // re-run only when `remaining` changes

  return (
    <>
      <p>{remaining} todos left</p>
      <button onClick={() => setTodos(todos.slice(1))} disabled={remaining === 0}>
        Complete one
      </button>
    </>
  );
}

The second argument is the dependency array: React re-runs the effect only when one of these values has changed since the last render.

Effects can also return a cleanup function, which React calls before re-running the effect and when the component is removed:

JSX
import { useEffect, useState } from "react";

export default function Clock() {
  const [now, setNow] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id); // cleanup: stop the timer
  }, []); // [] = set up once when the component appears

  return <p>{now.toLocaleTimeString()}</p>;
}

That's enough to get started. The useEffect & the component lifecycle lesson covers dependency arrays, cleanup, Strict Mode's double-run and — importantly — when you don't need an effect.

Common mistakes#

  • Forgetting the key, or putting it on the wrong element (it goes on the outermost element returned from map).
  • Using array indexes as keys for lists that change.
  • Generating keys with Math.random() during render.
  • Sorting state in place with .sort() — that mutates it. Use toSorted.
  • Storing filtered or sorted copies in state instead of deriving them.

What's next#

Lists and keys done. Next, collect user input properly with forms and controlled inputs.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Why does React need a key on list items?

  2. 2.Which is the best key for a list of products from an API?

  3. 3.When does useEffect(fn, []) run fn?

Finished reading?

Mark this lesson complete to track your progress.