Skip to main content

Arrays become lists

Most real-world React is rendering lists — users, products, messages, notifications. You already know .map() from the array methods lesson. In React, .map() turns an array of data into an array of JSX elements.
function UserList() {
  const users = [
    { id: 1, name: "Sarah Chen", email: "sarah@example.com" },
    { id: 2, name: "John Park", email: "john@example.com" },
    { id: 3, name: "Maria Lopez", email: "maria@example.com" },
  ];

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}{user.email}
        </li>
      ))}
    </ul>
  );
}
That’s the entire pattern: .map() over an array, return JSX for each item, add a key prop. You’ll use this in virtually every React component.

The key prop

Every item in a list needs a unique key prop. React uses keys to track which items changed, were added, or were removed.
// ✅ Use a unique, stable identifier
{users.map(user => (
  <li key={user.id}>{user.name}</li>
))}

// ✅ Any unique string works
{emails.map(email => (
  <li key={email}>{email}</li>
))}

What makes a good key?

Key sourceGood?Why
Database ID (user.id)BestUnique and stable across re-renders
Unique field (user.email)GoodUnique, but may change
Array indexLast resortChanges when items are reordered/deleted
Math.random()NeverDifferent every render, breaks everything
// ❌ Avoid index as key when items can be reordered or deleted
{users.map((user, index) => (
  <li key={index}>{user.name}</li>
))}

// ✅ Use the item's unique identifier
{users.map(user => (
  <li key={user.id}>{user.name}</li>
))}
If you don’t provide a key, React will warn you in the console. If you use array index as a key and items get reordered, React will mix up which component goes with which data — causing subtle, hard-to-debug UI bugs.

Rendering components from a list

Usually you render a component for each item, not raw HTML:
function UserCard({ name, email, role }) {
  return (
    <div className="user-card">
      <h3>{name}</h3>
      <p>{email}</p>
      <span className="badge">{role}</span>
    </div>
  );
}

function UserList({ users }) {
  return (
    <div className="user-list">
      {users.map(user => (
        <UserCard
          key={user.id}
          name={user.name}
          email={user.email}
          role={user.role}
        />
      ))}
    </div>
  );
}
The key goes on the outermost element in the .map() — which is the <UserCard> component, not any element inside it. React needs the key on the mapped element, not on a child within it.

Filtering and transforming before rendering

Process your data before the return statement. Don’t try to do complex logic inside JSX:
function UserList({ users, searchTerm }) {
  // Filter first, then render
  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

  if (filteredUsers.length === 0) {
    return <p>No users match "{searchTerm}"</p>;
  }

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
// Sort, filter, transform — all before the return
function ProductList({ products }) {
  const availableProducts = products
    .filter(p => p.inStock)
    .sort((a, b) => a.price - b.price);

  return (
    <div>
      <h2>Available Products ({availableProducts.length})</h2>
      {availableProducts.map(product => (
        <div key={product.id}>
          <span>{product.name}</span>
          <span>${product.price}</span>
        </div>
      ))}
    </div>
  );
}
Do filtering, sorting, and transforming above the return statement, not inside JSX. It keeps your JSX clean and readable, and you can handle the empty state separately.

A searchable list — putting it together

This combines state, effects, list rendering, and conditional rendering:
import { useState, useEffect } from 'react';

function SearchableUserList() {
  const [users, setUsers] = useState([]);
  const [search, setSearch] = useState("");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadUsers() {
      try {
        const response = await fetch("/api/users");
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        setUsers(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    loadUsers();
  }, []);

  if (loading) return <p>Loading users...</p>;
  if (error) return <p>Error: {error}</p>;

  const filtered = users.filter(user =>
    user.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        value={search}
        onChange={e => setSearch(e.target.value)}
      />

      {filtered.length === 0 ? (
        <p>No users match "{search}"</p>
      ) : (
        <ul>
          {filtered.map(user => (
            <li key={user.id}>
              {user.name}{user.email}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
This is a realistic component. Fetch data, let the user search it, render the filtered results. This pattern appears everywhere in production React apps.

Common mistakes

// ❌ Missing key — React warns in console
{users.map(user => (
  <li>{user.name}</li>
))}

// ✅ Always provide a key
{users.map(user => (
  <li key={user.id}>{user.name}</li>
))}
React needs keys to track which item is which across renders. Without stable keys, React can mix up item identity (especially when inserting, deleting, or reordering) and do unnecessary work.
// ❌ Key is on <li> inside the component, not on the mapped element
{users.map(user => (
  <UserCard user={user} />  // Missing key here!
))}

function UserCard({ user }) {
  return <li key={user.id}>{user.name}</li>; // Key here does nothing
}

// ✅ Key goes on the element returned by .map()
{users.map(user => (
  <UserCard key={user.id} user={user} />
))}
// ❌ Missing parentheses — returns undefined
{users.map(user => {
  <li key={user.id}>{user.name}</li>
})}

// ✅ Use parentheses for implicit return
{users.map(user => (
  <li key={user.id}>{user.name}</li>
))}

// ✅ Or use explicit return with curly braces
{users.map(user => {
  return <li key={user.id}>{user.name}</li>;
})}
Arrow functions with {} need an explicit return. Arrow functions with () implicitly return the expression. This is a JavaScript gotcha, not a React one — but it shows up constantly in .map() calls.

What’s next?

You can render lists of data. Now let’s learn how to handle user input — forms are how users interact with your app.

Forms in React

Build controlled forms that sync input values with React state