Skip to main content

Lists and Keys

Rendering Lists

Written by Published

Rendering a list is map. There is no loop syntax in JSX because an array of elements is already something React knows how to render.

map returns elements

Because a JSX element is just a value, mapping an array of data to an array of elements is ordinary JavaScript. React renders each item in order.

Data to elements

jsx

function List() {
  const people = [
    { id: 1, name: "Ada", role: "Engineer" },
    { id: 2, name: "Grace", role: "Admiral" },
    { id: 3, name: "Alan", role: "Logician" },
  ]

  return (
    <ul>
      {people.map((person) => (
        <li key={person.id}>
          <strong>{person.name}</strong>{person.role}
        </li>
      ))}
    </ul>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<List />)

Note the arrow returns the element directly, with parentheses rather than braces. Writing { opens a function body, and then you must return - forgetting to is a common way to render nothing.

Filtering and sorting

Both happen before the map, as ordinary array work. The one trap is sort, which mutates - always sort a copy, or you are mutating state.

Filter, then sort a copy

jsx

function Roster() {
  const [onlyActive, setOnlyActive] = React.useState(false)
  const people = [
    { id: 1, name: "Ada", active: true },
    { id: 2, name: "Grace", active: false },
    { id: 3, name: "Alan", active: true },
  ]

  const shown = people
    .filter((person) => (onlyActive ? person.active : true))
    .slice()                                  // copy before sorting
    .sort((a, b) => a.name.localeCompare(b.name))

  return (
    <div>
      <button onClick={() => setOnlyActive((v) => !v)}>
        {onlyActive ? "Show everyone" : "Only active"}
      </button>
      <ul>
        {shown.map((person) => <li key={person.id}>{person.name}</li>)}
      </ul>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Roster />)

The empty case

An empty array renders nothing at all - not an error, just silence. That is rarely what you want a user to see, so handle it explicitly.

Say something when there is nothing

jsx

function Items({ items }) {
  if (items.length === 0) {
    return <p style={{ color: "#64748b" }}>Nothing here yet.</p>
  }

  return <ul>{items.map((item) => <li key={item}>{item}</li>)}</ul>
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <div>
    <Items items={["one", "two"]} />
    <Items items={[]} />
  </div>
)

Resist {items.length && ...} here - that is the stray-zero bug from the previous section. An early return is clearer and cannot misfire.

Nested lists

A list inside a list is two maps, and each needs its own key. Keys only have to be unique among their immediate siblings, so an inner list can reuse the same values as another inner list without any conflict.

A map inside a map

jsx

function Groups() {
  const groups = [
    { id: "a", title: "Fruit", items: ["apple", "pear"] },
    { id: "b", title: "Tools", items: ["hammer", "saw"] },
  ]

  return (
    <div>
      {groups.map((group) => (
        <div key={group.id}>
          <strong>{group.title}</strong>
          <ul>
            {group.items.map((item) => <li key={item}>{item}</li>)}
          </ul>
        </div>
      ))}
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Groups />)
  • map data to elements; there is no loop syntax.
  • Use parentheses in the arrow, or remember to return.
  • sort mutates - copy first.
  • Handle the empty array deliberately.
  • Nested lists need a key at each level; siblings are the only scope.