React.js Fundamentals

Lists & Keys

15 min Lesson 9 of 40

Rendering Lists in React

In React, you'll often need to display multiple similar components from a collection of data. The most common way to do this is using JavaScript's map() function to transform an array of data into an array of components.

Core Concept: When rendering lists in React, each item needs a unique "key" prop. Keys help React identify which items have changed, been added, or been removed, enabling efficient updates to the DOM.

Basic List Rendering with map()

The map() method creates a new array by calling a function on every element in the original array. In React, we use it to transform data into JSX elements:

function NumberList() { const numbers = [1, 2, 3, 4, 5]; return ( <ul> {numbers.map((number) => ( <li key={number}>{number}</li> ))} </ul> ); } // With objects function UserList() { const users = [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 }, { id: 3, name: 'Charlie', age: 35 } ]; return ( <div> {users.map((user) => ( <div key={user.id} className="user-card"> <h3>{user.name}</h3> <p>Age: {user.age}</p> </div> ))} </div> ); }

Understanding Keys

Keys are special attributes that help React identify which items in a list have changed. They should be stable, predictable, and unique among siblings.

function TodoList() { const todos = [ { id: 'todo-1', text: 'Learn React', completed: false }, { id: 'todo-2', text: 'Build a project', completed: false }, { id: 'todo-3', text: 'Deploy to production', completed: true } ]; return ( <ul> {todos.map((todo) => ( <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text} </li> ))} </ul> ); }
Best Practice: Use stable, unique identifiers (like database IDs) as keys. Keys should not change between renders - they should be inherent to the data itself, not generated on the fly.

Why Keys Matter

Keys tell React which array item each component corresponds to, so it can match them up later. This is important if your array items can move, be deleted, or have items inserted.

// BAD: Without keys, React can't track items properly function BadList({ items }) { return ( <ul> {items.map((item) => ( <li>{item.name}</li> {/* Missing key! */} ))} </ul> ); } // GOOD: With proper keys function GoodList({ items }) { return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }

Extracting Components with Keys

When extracting a component from a list, keep the key on the component in the map() call, not in the component itself:

// Component definition - no key here function ListItem({ todo }) { return ( <li> <strong>{todo.title}</strong>: {todo.description} </li> ); } // Parent component - key goes here function TodoList() { const todos = [ { id: 1, title: 'Shopping', description: 'Buy groceries' }, { id: 2, title: 'Study', description: 'Read React docs' }, { id: 3, title: 'Exercise', description: 'Go for a run' } ]; return ( <ul> {todos.map((todo) => ( <ListItem key={todo.id} todo={todo} /> ))} </ul> ); }
Common Mistake: Never use array index as a key if the list can be reordered, filtered, or have items added/removed. Using index as key can cause subtle bugs and performance issues. Only use index as a last resort for static lists that never change.

Index as Key - When It's Acceptable

Using the array index as a key is acceptable only in these specific cases:

// ACCEPTABLE: Static list that never changes function MonthList() { const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; return ( <ul> {months.map((month, index) => ( <li key={index}>{month}</li> ))} </ul> ); } // BAD: Dynamic list - items can be reordered function DynamicList({ items }) { return ( <ul> {items.map((item, index) => ( <li key={index}>{item.name}</li> {/* Problematic! */} ))} </ul> ); } // GOOD: Use unique IDs for dynamic lists function DynamicListFixed({ items }) { return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> ); }

Generating Unique IDs

If your data doesn't have unique IDs, you can generate them. Here are some approaches:

import { useState } from 'react'; function TodoApp() { const [todos, setTodos] = useState([]); const [input, setInput] = useState(''); // Method 1: Using Date.now() + random const addTodo1 = () => { const newTodo = { id: Date.now() + Math.random(), // Unique enough for most cases text: input }; setTodos([...todos, newTodo]); setInput(''); }; // Method 2: Using crypto.randomUUID() (modern browsers) const addTodo2 = () => { const newTodo = { id: crypto.randomUUID(), // Native browser function text: input }; setTodos([...todos, newTodo]); setInput(''); }; // Method 3: Counter-based IDs let nextId = 0; const addTodo3 = () => { const newTodo = { id: `todo-${nextId++}`, // Simple incremental ID text: input }; setTodos([...todos, newTodo]); setInput(''); }; return ( <div> <input value={input} onChange={(e) => setInput(e.target.value)} /> <button onClick={addTodo1}>Add</button> <ul> {todos.map((todo) => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </div> ); }

Nested Lists

You can have lists inside lists. Each level needs its own unique keys:

function CategoryList() { const categories = [ { id: 'cat-1', name: 'Fruits', items: [ { id: 'item-1', name: 'Apple' }, { id: 'item-2', name: 'Banana' }, { id: 'item-3', name: 'Orange' } ] }, { id: 'cat-2', name: 'Vegetables', items: [ { id: 'item-4', name: 'Carrot' }, { id: 'item-5', name: 'Broccoli' }, { id: 'item-6', name: 'Spinach' } ] } ]; return ( <div> {categories.map((category) => ( <div key={category.id} className="category"> <h2>{category.name}</h2> <ul> {category.items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> ))} </div> ); }
Key Scope: Keys only need to be unique among siblings, not globally. Two different lists can use the same keys without conflict.

Filtering and Transforming Lists

You can combine filter(), map(), and other array methods to manipulate lists before rendering:

function ProductList() { const products = [ { id: 1, name: 'Laptop', price: 999, inStock: true }, { id: 2, name: 'Mouse', price: 25, inStock: true }, { id: 3, name: 'Keyboard', price: 75, inStock: false }, { id: 4, name: 'Monitor', price: 299, inStock: true }, { id: 5, name: 'Webcam', price: 89, inStock: false } ]; // Filter out of stock products const inStockProducts = products.filter(product => product.inStock); // Sort by price const sortedProducts = [...products].sort((a, b) => a.price - b.price); // Get only expensive items (>$100) const expensiveProducts = products.filter(product => product.price > 100); return ( <div> <h2>In Stock Products</h2> <ul> {inStockProducts.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> <h2>Sorted by Price</h2> <ul> {sortedProducts.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> <h2>Premium Products</h2> <ul> {expensiveProducts.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> </div> ); }

Handling Empty Lists

Always consider what to display when a list is empty:

function MessageList({ messages }) { return ( <div> <h2>Messages</h2> {messages.length === 0 ? ( <p className="empty-state">No messages yet. Start a conversation!</p> ) : ( <ul> {messages.map((message) => ( <li key={message.id}> <strong>{message.sender}:</strong> {message.text} </li> ))} </ul> )} </div> ); }
User Experience: Always provide helpful feedback when lists are empty. Empty states are an opportunity to guide users on what to do next or explain why the list is empty.

Exercise 1: Student Grade List

Create a component that displays a list of students with their grades:

  • Array of student objects: { id, name, grade, passed }
  • Display each student's name and grade
  • Show "PASSED" in green or "FAILED" in red based on passed status
  • Add a filter to show only passed students or only failed students
  • Add a sort button to sort by grade (highest to lowest)
  • Display total number of students and pass percentage

Hint: Use filter() and sort() before map().

Exercise 2: Nested Comment Thread

Build a comment system with nested replies:

  • Each comment has: id, author, text, and replies (array of comment objects)
  • Display comments with their replies indented below
  • Replies can have their own replies (recursive structure)
  • Add "Reply" button to each comment (can use alert for now)
  • Show comment count for each thread
  • Style differently based on nesting level

Bonus: Create a recursive component that can handle any nesting depth.

Exercise 3: E-commerce Product Catalog

Create a product catalog with multiple filtering and sorting options:

  • Products array with: id, name, price, category, rating, inStock
  • Display products in a grid layout
  • Filter by category (Electronics, Clothing, Books, etc.)
  • Filter by availability (In Stock / Out of Stock)
  • Sort by: Price (low to high / high to low), Rating, Name (A-Z)
  • Show "Out of Stock" overlay on unavailable products
  • Display total product count and average price of filtered results

Hint: Chain multiple filter() and sort() operations based on user selections.