Skip to main content

Lists & Keys

Rendering lists of data is one of the most common tasks in React. Understanding how to do it correctly and efficiently is crucial for building performant applications.

Rendering Lists with map()

Use JavaScript’s map() method to transform an array of data into an array of elements.
function SimpleList() {
  const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];

  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  );
}

Rendering Objects

function UserList() {
  const users = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' },
    { id: 3, name: 'Charlie', email: '[email protected]' }
  ];

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <strong>{user.name}</strong> - {user.email}
        </li>
      ))}
    </ul>
  );
}

Understanding Keys

Keys are special string attributes you need to include when creating lists of elements. They help React identify which items have changed, been added, or been removed.

Why Keys Matter

React uses keys during its reconciliation process to efficiently update the DOM:
Without Keys:
┌──────────────────────────────────────────────────────────┐
│  Old List        Change         New List                │
│  ┌─────────┐                   ┌─────────┐              │
│  │ Item A  │    Insert X      │ Item X  │ ← Recreated  │
│  ├─────────┤    at start      ├─────────┤              │
│  │ Item B  │                  │ Item A  │ ← Recreated  │
│  ├─────────┤                  ├─────────┤              │
│  │ Item C  │                  │ Item B  │ ← Recreated  │
│  └─────────┘                  ├─────────┤              │
│                               │ Item C  │ ← Recreated  │
│                               └─────────┘              │
│  React rebuilds EVERYTHING (O(n) operations)           │
└──────────────────────────────────────────────────────────┘

With Keys:
┌──────────────────────────────────────────────────────────┐
│  Old List        Change         New List                │
│  ┌─────────┐                   ┌─────────┐              │
│  │ key="a" │    Insert X      │ key="x" │ ← NEW        │
│  ├─────────┤    at start      ├─────────┤              │
│  │ key="b" │                  │ key="a" │ ← Moved      │
│  ├─────────┤                  ├─────────┤              │
│  │ key="c" │                  │ key="b" │ ← Moved      │
│  └─────────┘                  ├─────────┤              │
│                               │ key="c" │ ← Moved      │
│                               └─────────┘              │
│  React only INSERTS the new item (O(1) operation)      │
└──────────────────────────────────────────────────────────┘

Key Rules

  1. Keys must be unique among siblings (not globally unique)
  2. Keys should be stable (same item = same key across renders)
  3. Keys should not change (don’t use random values)

What to Use as Keys

// ✅ BEST: Database IDs
{users.map(user => <UserCard key={user.id} user={user} />)}

// ✅ GOOD: Unique identifiers in data
{products.map(product => <ProductCard key={product.sku} product={product} />)}

// ✅ OK: Generated unique IDs (when adding items)
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
  setTodos([...todos, { id: crypto.randomUUID(), text }]);
};

// ⚠️ CAUTION: Index (only for static lists that never reorder)
{staticItems.map((item, index) => <li key={index}>{item}</li>)}

// ❌ BAD: Random values
{items.map(item => <li key={Math.random()}>{item}</li>)}
Never use Math.random() or Date.now() as keys! These change on every render, causing React to recreate all elements, destroying component state and causing performance issues.

The Index Key Problem

Using array index as key causes bugs when the list order changes:
// ❌ PROBLEMATIC - State gets mixed up when reordering
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Build project' }
  ]);

  const moveToTop = (id) => {
    const todo = todos.find(t => t.id === id);
    setTodos([todo, ...todos.filter(t => t.id !== id)]);
  };

  return (
    <ul>
      {todos.map((todo, index) => (
        // Using index as key - BAD when reordering!
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  );
}

// ✅ CORRECT - Use unique ID
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}

Filtering Lists

function FilterableProductList() {
  const [filter, setFilter] = useState('all');
  
  const products = [
    { id: 1, name: 'iPhone', category: 'electronics', price: 999 },
    { id: 2, name: 'T-Shirt', category: 'clothing', price: 29 },
    { id: 3, name: 'MacBook', category: 'electronics', price: 1999 },
    { id: 4, name: 'Jeans', category: 'clothing', price: 79 }
  ];

  const filteredProducts = filter === 'all' 
    ? products 
    : products.filter(p => p.category === filter);

  return (
    <div>
      <div className="filters">
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('electronics')}>Electronics</button>
        <button onClick={() => setFilter('clothing')}>Clothing</button>
      </div>
      
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
      
      {filteredProducts.length === 0 && (
        <p>No products match this filter.</p>
      )}
    </div>
  );
}

Sorting Lists

function SortableTable() {
  const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
  
  const users = [
    { id: 1, name: 'Charlie', age: 30, email: '[email protected]' },
    { id: 2, name: 'Alice', age: 25, email: '[email protected]' },
    { id: 3, name: 'Bob', age: 35, email: '[email protected]' }
  ];

  const sortedUsers = [...users].sort((a, b) => {
    if (a[sortConfig.key] < b[sortConfig.key]) {
      return sortConfig.direction === 'asc' ? -1 : 1;
    }
    if (a[sortConfig.key] > b[sortConfig.key]) {
      return sortConfig.direction === 'asc' ? 1 : -1;
    }
    return 0;
  });

  const handleSort = (key) => {
    setSortConfig(prev => ({
      key,
      direction: prev.key === key && prev.direction === 'asc' ? 'desc' : 'asc'
    }));
  };

  const SortIcon = ({ column }) => {
    if (sortConfig.key !== column) return '↕️';
    return sortConfig.direction === 'asc' ? '↑' : '↓';
  };

  return (
    <table>
      <thead>
        <tr>
          <th onClick={() => handleSort('name')}>
            Name <SortIcon column="name" />
          </th>
          <th onClick={() => handleSort('age')}>
            Age <SortIcon column="age" />
          </th>
          <th onClick={() => handleSort('email')}>
            Email <SortIcon column="email" />
          </th>
        </tr>
      </thead>
      <tbody>
        {sortedUsers.map(user => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.age}</td>
            <td>{user.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Search and Filter Combined

function SearchableList() {
  const [searchTerm, setSearchTerm] = useState('');
  
  const items = [
    { id: 1, title: 'React Fundamentals', tags: ['react', 'javascript'] },
    { id: 2, title: 'Node.js Basics', tags: ['node', 'javascript'] },
    { id: 3, title: 'CSS Grid Layout', tags: ['css', 'design'] },
    { id: 4, title: 'TypeScript Guide', tags: ['typescript', 'javascript'] }
  ];

  const filteredItems = items.filter(item => {
    const matchesTitle = item.title.toLowerCase().includes(searchTerm.toLowerCase());
    const matchesTags = item.tags.some(tag => 
      tag.toLowerCase().includes(searchTerm.toLowerCase())
    );
    return matchesTitle || matchesTags;
  });

  return (
    <div>
      <input
        type="text"
        placeholder="Search by title or tag..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      
      <p>{filteredItems.length} results found</p>
      
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>
            <strong>{item.title}</strong>
            <div className="tags">
              {item.tags.map(tag => (
                <span key={tag} className="tag">{tag}</span>
              ))}
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

Nested Lists

function CategoryList() {
  const categories = [
    {
      id: 1,
      name: 'Electronics',
      products: [
        { id: 101, name: 'Laptop' },
        { id: 102, name: 'Phone' }
      ]
    },
    {
      id: 2,
      name: 'Clothing',
      products: [
        { id: 201, name: 'T-Shirt' },
        { id: 202, name: 'Jeans' }
      ]
    }
  ];

  return (
    <div>
      {categories.map(category => (
        <div key={category.id} className="category">
          <h2>{category.name}</h2>
          <ul>
            {category.products.map(product => (
              <li key={product.id}>{product.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}
Keys only need to be unique among siblings. In the example above, product.id can be the same number as category.id because they’re in different levels.

Rendering Nothing for Some Items

Use filter() before map(), or return null from map:
function ActiveUsersList({ users }) {
  // Method 1: Filter then map (preferred)
  return (
    <ul>
      {users
        .filter(user => user.isActive)
        .map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
    </ul>
  );
}

function ConditionalList({ items }) {
  // Method 2: Return null for items to skip
  return (
    <ul>
      {items.map(item => {
        if (item.isHidden) return null;
        return <li key={item.id}>{item.name}</li>;
      })}
    </ul>
  );
}

Empty States

Always handle the case when a list is empty:
function UserList({ users }) {
  if (users.length === 0) {
    return (
      <div className="empty-state">
        <img src="/empty.svg" alt="No users" />
        <h3>No users found</h3>
        <p>Try adjusting your search or filters.</p>
        <button>Clear filters</button>
      </div>
    );
  }

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Performance: Extracting List Items

For complex lists, extract the item into its own component:
// ✅ Good - Extracted component
function ProductCard({ product, onAddToCart }) {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => onAddToCart(product.id)}>
        Add to Cart
      </button>
    </div>
  );
}

function ProductGrid({ products, onAddToCart }) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
}
Performance Tip: When a list item is its own component, you can wrap it with React.memo() to prevent re-renders when other items change.
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
  // Only re-renders if product or onAddToCart changes
  return (/* ... */);
});

🎯 Practice Exercises

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build project', completed: false }
  ]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    if (!input.trim()) return;
    setTodos([...todos, {
      id: Date.now(),
      text: input,
      completed: false
    }]);
    setInput('');
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  const pendingCount = todos.filter(t => !t.completed).length;

  return (
    <div>
      <h1>Todos ({pendingCount} pending)</h1>
      
      <div>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && addTodo()}
          placeholder="Add todo..."
        />
        <button onClick={addTodo}>Add</button>
      </div>

      {todos.length === 0 ? (
        <p>No todos yet!</p>
      ) : (
        <ul>
          {todos.map(todo => (
            <li key={todo.id}>
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => toggleTodo(todo.id)}
              />
              <span style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}>
                {todo.text}
              </span>
              <button onClick={() => deleteTodo(todo.id)}>🗑️</button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
function ContactList() {
  const [search, setSearch] = useState('');
  const [filter, setFilter] = useState('all'); // all, favorites

  const contacts = [
    { id: 1, name: 'Alice Johnson', phone: '555-0101', favorite: true },
    { id: 2, name: 'Bob Smith', phone: '555-0102', favorite: false },
    { id: 3, name: 'Charlie Brown', phone: '555-0103', favorite: true },
    { id: 4, name: 'Diana Ross', phone: '555-0104', favorite: false }
  ];

  const filteredContacts = contacts
    .filter(c => filter === 'all' || c.favorite)
    .filter(c => 
      c.name.toLowerCase().includes(search.toLowerCase()) ||
      c.phone.includes(search)
    );

  return (
    <div>
      <input
        type="text"
        placeholder="Search contacts..."
        value={search}
        onChange={e => setSearch(e.target.value)}
      />
      
      <div>
        <button 
          className={filter === 'all' ? 'active' : ''}
          onClick={() => setFilter('all')}
        >
          All
        </button>
        <button 
          className={filter === 'favorites' ? 'active' : ''}
          onClick={() => setFilter('favorites')}
        >
          ⭐ Favorites
        </button>
      </div>

      <ul>
        {filteredContacts.map(contact => (
          <li key={contact.id}>
            {contact.favorite && '⭐'} {contact.name} - {contact.phone}
          </li>
        ))}
      </ul>
      
      {filteredContacts.length === 0 && (
        <p>No contacts found.</p>
      )}
    </div>
  );
}
function ReorderableList() {
  const [items, setItems] = useState([
    { id: 1, text: 'First item' },
    { id: 2, text: 'Second item' },
    { id: 3, text: 'Third item' },
    { id: 4, text: 'Fourth item' }
  ]);

  const moveUp = (index) => {
    if (index === 0) return;
    const newItems = [...items];
    [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]];
    setItems(newItems);
  };

  const moveDown = (index) => {
    if (index === items.length - 1) return;
    const newItems = [...items];
    [newItems[index], newItems[index + 1]] = [newItems[index + 1], newItems[index]];
    setItems(newItems);
  };

  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id} style={{ display: 'flex', gap: '10px', padding: '8px' }}>
          <span>{item.text}</span>
          <button 
            onClick={() => moveUp(index)}
            disabled={index === 0}
          >

          </button>
          <button 
            onClick={() => moveDown(index)}
            disabled={index === items.length - 1}
          >

          </button>
        </li>
      ))}
    </ul>
  );
}

Summary

ConceptDescription
map()Transform array data into React elements
keyUnique identifier for list items (required)
ID as keyBest practice - use database IDs or stable unique identifiers
Index as keyOnly for static lists that never reorder
filter()Filter items before rendering
sort()Always use [...arr].sort() (don’t mutate)
Empty statesHandle zero results gracefully
Nested listsKeys only need to be unique among siblings
Extracted componentsBetter organization and potential performance gains

Next Steps

In the next chapter, you’ll learn about Forms & Controlled Components — handling user input effectively!