Episode 2 — React Frontend Architecture NextJS / 2.2 — React Components and Props

2.2.d — Rendering Lists

In one sentence: Rendering lists in React means transforming arrays of data into arrays of JSX elements using Array.map(), which is how you display repeated UI patterns like product grids, todo items, and comment threads.

Navigation: ← 2.2.c — Dynamic Rendering · Next → 2.2.e — Keys in React


Table of Contents

  1. Why Lists Are Everywhere in UIs
  2. The map() Method — React's List Engine
  3. Rendering Simple Arrays
  4. Rendering Arrays of Objects
  5. Extracting List Items into Components
  6. Filtering Before Rendering
  7. Sorting Before Rendering
  8. Chaining filter, sort, and map
  9. Nested Lists
  10. Empty State Handling
  11. Loading States for Lists
  12. Grouping List Items
  13. Pagination and Load More Patterns
  14. Common Mistakes with Lists
  15. Key Takeaways
  16. Explain-It Challenge

1. Why Lists Are Everywhere in UIs

Almost every application displays collections of similar items:

┌──────────────────────────────────────────────────────────┐
│               LISTS IN REAL APPLICATIONS                  │
│                                                          │
│  Twitter/X        → List of tweets                       │
│  Amazon           → Grid of products                     │
│  Gmail            → List of emails                       │
│  Spotify          → List of songs / playlists            │
│  GitHub           → List of repos / commits / issues     │
│  Slack            → List of messages                     │
│  Netflix          → Grid of movies/shows                 │
│  Reddit           → List of posts with comments          │
│  Trello           → Lists of cards in columns            │
│                                                          │
│  Pattern: Same UI template, repeated with different data │
└──────────────────────────────────────────────────────────┘

Without map(), you'd have to manually write every list item:

// ❌ Without map — hardcoded, doesn't scale
function UserList() {
  return (
    <ul>
      <li>Alice</li>
      <li>Bob</li>
      <li>Charlie</li>
    </ul>
  );
}

// ✅ With map — dynamic, scales to any size
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2. The map() Method — React's List Engine

Array.map() transforms each element of an array into something else. In React, we transform data into JSX.

How map() Works

// Regular JavaScript
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
// Result: [2, 4, 6, 8, 10]

// React — map data to JSX
const names = ["Alice", "Bob", "Charlie"];
const listItems = names.map(name => <li key={name}>{name}</li>);
// Result: [<li>Alice</li>, <li>Bob</li>, <li>Charlie</li>]

// React renders arrays of JSX automatically
function NameList() {
  const names = ["Alice", "Bob", "Charlie"];
  return (
    <ul>
      {names.map(name => (
        <li key={name}>{name}</li>
      ))}
    </ul>
  );
}

map() Parameters

array.map((element, index, originalArray) => {
  // element   — the current item
  // index     — position (0-based)
  // originalArray — the full array (rarely used)
  return <SomeJSX />;
});

// Example with all parameters
function RankedList({ players }) {
  return (
    <ol>
      {players.map((player, index) => (
        <li key={player.id}>
          #{index + 1} — {player.name} ({player.score} pts)
        </li>
      ))}
    </ol>
  );
}

3. Rendering Simple Arrays

Array of Strings

function TagList({ tags }) {
  return (
    <div className="tag-list">
      {tags.map(tag => (
        <span key={tag} className="tag">
          {tag}
        </span>
      ))}
    </div>
  );
}

<TagList tags={["React", "JavaScript", "Frontend", "Web Dev"]} />

Array of Numbers

function ScoreBoard({ scores }) {
  const average = (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1);
  
  return (
    <div>
      <h3>Scores (Avg: {average})</h3>
      <div className="scores">
        {scores.map((score, index) => (
          <span 
            key={index} 
            className="score-bubble"
            style={{
              backgroundColor: score >= 90 ? "#4caf50" : score >= 70 ? "#ff9800" : "#f44336",
              color: "white",
              padding: "4px 12px",
              borderRadius: "16px",
              margin: "4px",
              display: "inline-block",
            }}
          >
            {score}
          </span>
        ))}
      </div>
    </div>
  );
}

<ScoreBoard scores={[95, 82, 67, 91, 78, 88]} />

4. Rendering Arrays of Objects

This is the most common pattern in real apps — arrays of objects from an API.

Basic Object List

function UserList({ users }) {
  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-item">
          <img src={user.avatar} alt={user.name} className="avatar" />
          <div>
            <h3>{user.name}</h3>
            <p>{user.email}</p>
            <span className={`badge badge-${user.role}`}>{user.role}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

// Data
const users = [
  { id: 1, name: "Alice", email: "alice@example.com", role: "admin", avatar: "/alice.jpg" },
  { id: 2, name: "Bob", email: "bob@example.com", role: "user", avatar: "/bob.jpg" },
  { id: 3, name: "Charlie", email: "charlie@example.com", role: "moderator", avatar: "/charlie.jpg" },
];

<UserList users={users} />

Product Grid

function ProductGrid({ products }) {
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
      gap: "24px",
      padding: "24px",
    }}>
      {products.map(product => (
        <div key={product.id} className="product-card" style={{
          border: "1px solid #e0e0e0",
          borderRadius: "8px",
          overflow: "hidden",
        }}>
          <img 
            src={product.image} 
            alt={product.name}
            style={{ width: "100%", height: "200px", objectFit: "cover" }}
          />
          <div style={{ padding: "16px" }}>
            <h3 style={{ margin: "0 0 8px" }}>{product.name}</h3>
            <p style={{ color: "#666", fontSize: "0.875rem" }}>
              {product.description}
            </p>
            <div style={{ 
              display: "flex", 
              justifyContent: "space-between", 
              alignItems: "center",
              marginTop: "12px" 
            }}>
              <span style={{ fontSize: "1.25rem", fontWeight: "bold" }}>
                ${product.price.toFixed(2)}
              </span>
              <button 
                disabled={!product.inStock}
                style={{
                  padding: "8px 16px",
                  backgroundColor: product.inStock ? "#2196f3" : "#ccc",
                  color: "white",
                  border: "none",
                  borderRadius: "4px",
                  cursor: product.inStock ? "pointer" : "not-allowed",
                }}
              >
                {product.inStock ? "Add to Cart" : "Out of Stock"}
              </button>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

5. Extracting List Items into Components

When list items get complex, extract them into their own component:

// ❌ BEFORE — everything inline in map()
function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul>
      {todos.map(todo => (
        <li 
          key={todo.id}
          style={{ 
            display: "flex", alignItems: "center", gap: "12px",
            padding: "12px", borderBottom: "1px solid #eee",
            textDecoration: todo.done ? "line-through" : "none",
            opacity: todo.done ? 0.5 : 1,
          }}
        >
          <input 
            type="checkbox" 
            checked={todo.done} 
            onChange={() => onToggle(todo.id)} 
          />
          <span style={{ flex: 1 }}>{todo.text}</span>
          <span style={{ fontSize: "0.75rem", color: "#999" }}>
            {new Date(todo.createdAt).toLocaleDateString()}
          </span>
          <button 
            onClick={() => onDelete(todo.id)}
            style={{ color: "#f44336", background: "none", border: "none", cursor: "pointer" }}
          >
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

// ✅ AFTER — extracted into a component
function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul style={{ listStyle: "none", padding: 0 }}>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </ul>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li style={{ 
      display: "flex", alignItems: "center", gap: "12px",
      padding: "12px", borderBottom: "1px solid #eee",
      textDecoration: todo.done ? "line-through" : "none",
      opacity: todo.done ? 0.5 : 1,
    }}>
      <input 
        type="checkbox" 
        checked={todo.done} 
        onChange={() => onToggle(todo.id)} 
      />
      <span style={{ flex: 1 }}>{todo.text}</span>
      <span style={{ fontSize: "0.75rem", color: "#999" }}>
        {new Date(todo.createdAt).toLocaleDateString()}
      </span>
      <button 
        onClick={() => onDelete(todo.id)}
        style={{ color: "#f44336", background: "none", border: "none", cursor: "pointer" }}
      >
        Delete
      </button>
    </li>
  );
}

Benefits of Extraction

BenefitDescription
ReadabilityParent component is clean; child handles its own rendering
ReusabilityTodoItem can be used elsewhere
TestingTest TodoItem in isolation with different props
PerformanceEasier to apply React.memo to the item component
MaintainabilityChanges to item layout are isolated

6. Filtering Before Rendering

Never filter inside JSX — do it before the return.

function FilteredUserList({ users, roleFilter, searchQuery }) {
  // Filter BEFORE rendering
  const filteredUsers = users.filter(user => {
    // Apply role filter
    if (roleFilter && roleFilter !== "all" && user.role !== roleFilter) {
      return false;
    }
    
    // Apply search filter
    if (searchQuery) {
      const query = searchQuery.toLowerCase();
      return (
        user.name.toLowerCase().includes(query) ||
        user.email.toLowerCase().includes(query)
      );
    }
    
    return true;
  });

  return (
    <div>
      <p>{filteredUsers.length} users found</p>
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>
            {user.name} — {user.role}
          </li>
        ))}
      </ul>
    </div>
  );
}

Filter with UI Controls

function ProductCatalog({ products }) {
  const [category, setCategory] = useState("all");
  const [minPrice, setMinPrice] = useState(0);
  const [maxPrice, setMaxPrice] = useState(Infinity);
  const [inStockOnly, setInStockOnly] = useState(false);

  const filtered = products.filter(p => {
    if (category !== "all" && p.category !== category) return false;
    if (p.price < minPrice || p.price > maxPrice) return false;
    if (inStockOnly && !p.inStock) return false;
    return true;
  });

  const categories = ["all", ...new Set(products.map(p => p.category))];

  return (
    <div style={{ display: "flex", gap: "24px" }}>
      {/* Sidebar filters */}
      <aside style={{ width: "250px" }}>
        <h3>Filters</h3>
        
        <label>Category</label>
        <select value={category} onChange={e => setCategory(e.target.value)}>
          {categories.map(cat => (
            <option key={cat} value={cat}>
              {cat === "all" ? "All Categories" : cat}
            </option>
          ))}
        </select>

        <label>
          <input 
            type="checkbox" 
            checked={inStockOnly} 
            onChange={e => setInStockOnly(e.target.checked)} 
          />
          In Stock Only
        </label>

        <p>{filtered.length} of {products.length} products</p>
      </aside>

      {/* Product grid */}
      <main style={{ flex: 1 }}>
        {filtered.length === 0 ? (
          <EmptyState message="No products match your filters" />
        ) : (
          <div className="product-grid">
            {filtered.map(product => (
              <ProductCard key={product.id} product={product} />
            ))}
          </div>
        )}
      </main>
    </div>
  );
}

7. Sorting Before Rendering

Like filtering, always sort before rendering — and never mutate the original array.

function SortableTable({ data, columns }) {
  const [sortField, setSortField] = useState(null);
  const [sortDirection, setSortDirection] = useState("asc");

  const handleSort = (field) => {
    if (sortField === field) {
      setSortDirection(d => d === "asc" ? "desc" : "asc");
    } else {
      setSortField(field);
      setSortDirection("asc");
    }
  };

  // Create a sorted copy — NEVER mutate the original
  const sortedData = [...data].sort((a, b) => {
    if (!sortField) return 0;
    
    const aVal = a[sortField];
    const bVal = b[sortField];
    
    if (typeof aVal === "string") {
      return sortDirection === "asc"
        ? aVal.localeCompare(bVal)
        : bVal.localeCompare(aVal);
    }
    
    return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
  });

  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => (
            <th 
              key={col.field}
              onClick={() => handleSort(col.field)}
              style={{ cursor: "pointer", userSelect: "none" }}
            >
              {col.label}
              {sortField === col.field && (
                <span>{sortDirection === "asc" ? " ▲" : " ▼"}</span>
              )}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {sortedData.map(row => (
          <tr key={row.id}>
            {columns.map(col => (
              <td key={col.field}>{row[col.field]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Usage
<SortableTable
  data={employees}
  columns={[
    { field: "name", label: "Name" },
    { field: "department", label: "Department" },
    { field: "salary", label: "Salary" },
    { field: "startDate", label: "Start Date" },
  ]}
/>

8. Chaining filter, sort, and map

The power of arrays: chain operations together.

function LeaderBoard({ players, minScore = 0 }) {
  return (
    <ol>
      {players
        // 1. Filter: only players above minimum score
        .filter(player => player.score >= minScore)
        // 2. Sort: highest score first
        .sort((a, b) => b.score - a.score)
        // 3. Limit: top 10 only
        .slice(0, 10)
        // 4. Map: render each item
        .map((player, index) => (
          <li key={player.id} style={{
            display: "flex",
            justifyContent: "space-between",
            padding: "8px 16px",
            backgroundColor: index === 0 ? "#fff8e1" : index < 3 ? "#f5f5f5" : "transparent",
          }}>
            <span>
              {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `#${index + 1}`}
              {" "}{player.name}
            </span>
            <strong>{player.score.toLocaleString()} pts</strong>
          </li>
        ))
      }
    </ol>
  );
}

Pipeline Summary

┌──────────────────────────────────────────────────────────┐
│              DATA → UI PIPELINE                           │
│                                                          │
│  Raw data (from API/state)                               │
│       │                                                  │
│       ▼                                                  │
│  .filter()  — Remove items that don't match criteria     │
│       │                                                  │
│       ▼                                                  │
│  .sort()    — Order items by desired field               │
│       │                                                  │
│       ▼                                                  │
│  .slice()   — Limit to N items (pagination)              │
│       │                                                  │
│       ▼                                                  │
│  .map()     — Transform each item into JSX               │
│       │                                                  │
│       ▼                                                  │
│  Rendered list in the DOM                                │
└──────────────────────────────────────────────────────────┘

9. Nested Lists

Two-Level Nesting

function CourseList({ departments }) {
  return (
    <div>
      {departments.map(dept => (
        <div key={dept.id} className="department">
          <h2>{dept.name}</h2>
          <ul>
            {dept.courses.map(course => (
              <li key={course.id}>
                <strong>{course.code}</strong> — {course.title}
                <span className="credits">({course.credits} credits)</span>
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

// Data
const departments = [
  {
    id: 1,
    name: "Computer Science",
    courses: [
      { id: 101, code: "CS101", title: "Intro to Programming", credits: 3 },
      { id: 102, code: "CS201", title: "Data Structures", credits: 4 },
    ],
  },
  {
    id: 2,
    name: "Mathematics",
    courses: [
      { id: 201, code: "MATH101", title: "Calculus I", credits: 4 },
      { id: 202, code: "MATH201", title: "Linear Algebra", credits: 3 },
    ],
  },
];

Recursive Lists (Tree Structure)

function FileTree({ items, depth = 0 }) {
  return (
    <ul style={{ listStyle: "none", paddingLeft: depth > 0 ? "20px" : "0" }}>
      {items.map(item => (
        <li key={item.path}>
          <div style={{ 
            padding: "4px 8px",
            cursor: item.type === "folder" ? "pointer" : "default",
          }}>
            {item.type === "folder" ? "📁" : "📄"} {item.name}
          </div>
          {/* Recursion: if folder has children, render another FileTree */}
          {item.type === "folder" && item.children && (
            <FileTree items={item.children} depth={depth + 1} />
          )}
        </li>
      ))}
    </ul>
  );
}

// Data
const fileSystem = [
  {
    name: "src", type: "folder", path: "/src",
    children: [
      { name: "App.jsx", type: "file", path: "/src/App.jsx" },
      {
        name: "components", type: "folder", path: "/src/components",
        children: [
          { name: "Header.jsx", type: "file", path: "/src/components/Header.jsx" },
          { name: "Footer.jsx", type: "file", path: "/src/components/Footer.jsx" },
        ],
      },
    ],
  },
  { name: "package.json", type: "file", path: "/package.json" },
];

10. Empty State Handling

Always handle the case where the list is empty.

function TaskList({ tasks, onAddTask }) {
  if (!tasks || tasks.length === 0) {
    return (
      <div style={{
        textAlign: "center",
        padding: "48px 24px",
        color: "#999",
      }}>
        <div style={{ fontSize: "3rem", marginBottom: "16px" }}>📋</div>
        <h3>No tasks yet</h3>
        <p>Create your first task to get started</p>
        <button 
          onClick={onAddTask}
          style={{
            marginTop: "16px",
            padding: "10px 24px",
            backgroundColor: "#2196f3",
            color: "white",
            border: "none",
            borderRadius: "6px",
            cursor: "pointer",
          }}
        >
          + Add Task
        </button>
      </div>
    );
  }

  return (
    <ul>
      {tasks.map(task => (
        <TaskItem key={task.id} task={task} />
      ))}
    </ul>
  );
}

Reusable Empty State Component

function EmptyState({ 
  icon = "📭", 
  title = "Nothing here", 
  description, 
  action,
  onAction 
}) {
  return (
    <div style={{ textAlign: "center", padding: "48px 24px" }}>
      <div style={{ fontSize: "3rem", marginBottom: "16px" }}>{icon}</div>
      <h3 style={{ marginBottom: "8px" }}>{title}</h3>
      {description && <p style={{ color: "#666" }}>{description}</p>}
      {action && onAction && (
        <button onClick={onAction} className="btn btn-primary" style={{ marginTop: "16px" }}>
          {action}
        </button>
      )}
    </div>
  );
}

// Usage
<EmptyState 
  icon="🔍"
  title="No results found"
  description="Try adjusting your search or filters"
  action="Clear Filters"
  onAction={handleClearFilters}
/>

11. Loading States for Lists

function UserList({ isLoading, error, users }) {
  // Loading state — show skeleton
  if (isLoading) {
    return (
      <div>
        {Array.from({ length: 5 }).map((_, i) => (
          <div key={i} className="skeleton" style={{
            display: "flex", gap: "12px", padding: "16px",
            borderBottom: "1px solid #f0f0f0",
          }}>
            <div style={{ 
              width: 48, height: 48, borderRadius: "50%", 
              backgroundColor: "#e0e0e0", animation: "pulse 1.5s infinite" 
            }} />
            <div style={{ flex: 1 }}>
              <div style={{ 
                width: "60%", height: 16, backgroundColor: "#e0e0e0", 
                borderRadius: 4, marginBottom: 8, animation: "pulse 1.5s infinite" 
              }} />
              <div style={{ 
                width: "40%", height: 12, backgroundColor: "#e0e0e0", 
                borderRadius: 4, animation: "pulse 1.5s infinite" 
              }} />
            </div>
          </div>
        ))}
      </div>
    );
  }

  // Error state
  if (error) {
    return (
      <div style={{ textAlign: "center", padding: "24px", color: "#f44336" }}>
        <p>Failed to load users: {error.message}</p>
        <button onClick={() => window.location.reload()}>Retry</button>
      </div>
    );
  }

  // Empty state
  if (users.length === 0) {
    return <EmptyState icon="👥" title="No users found" />;
  }

  // Success state
  return (
    <ul>
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

12. Grouping List Items

Group by Category

function GroupedContactList({ contacts }) {
  // Group contacts by first letter
  const grouped = contacts.reduce((groups, contact) => {
    const letter = contact.name[0].toUpperCase();
    if (!groups[letter]) groups[letter] = [];
    groups[letter].push(contact);
    return groups;
  }, {});

  // Sort the groups alphabetically
  const sortedLetters = Object.keys(grouped).sort();

  return (
    <div>
      {sortedLetters.map(letter => (
        <div key={letter}>
          <h3 style={{
            padding: "8px 16px",
            backgroundColor: "#f5f5f5",
            position: "sticky",
            top: 0,
            fontWeight: "bold",
          }}>
            {letter}
          </h3>
          <ul style={{ listStyle: "none", padding: 0 }}>
            {grouped[letter].map(contact => (
              <li key={contact.id} style={{ padding: "8px 16px", borderBottom: "1px solid #eee" }}>
                {contact.name}
              </li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

13. Pagination and Load More Patterns

Simple Pagination

function PaginatedList({ items, itemsPerPage = 10 }) {
  const [currentPage, setCurrentPage] = useState(1);
  
  const totalPages = Math.ceil(items.length / itemsPerPage);
  const startIndex = (currentPage - 1) * itemsPerPage;
  const currentItems = items.slice(startIndex, startIndex + itemsPerPage);

  return (
    <div>
      <ul>
        {currentItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>

      {/* Pagination controls */}
      <div style={{ display: "flex", gap: "8px", justifyContent: "center", marginTop: "24px" }}>
        <button 
          onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
          disabled={currentPage === 1}
        >
          Previous
        </button>
        
        {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
          <button
            key={page}
            onClick={() => setCurrentPage(page)}
            style={{
              fontWeight: page === currentPage ? "bold" : "normal",
              backgroundColor: page === currentPage ? "#2196f3" : "white",
              color: page === currentPage ? "white" : "black",
              border: "1px solid #e0e0e0",
              borderRadius: "4px",
              padding: "6px 12px",
              cursor: "pointer",
            }}
          >
            {page}
          </button>
        ))}
        
        <button 
          onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
          disabled={currentPage === totalPages}
        >
          Next
        </button>
      </div>

      <p style={{ textAlign: "center", color: "#999", marginTop: "8px" }}>
        Showing {startIndex + 1}–{Math.min(startIndex + itemsPerPage, items.length)} of {items.length}
      </p>
    </div>
  );
}

"Load More" Pattern

function InfiniteList({ fetchItems }) {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadMore = async () => {
    if (loading || !hasMore) return;
    setLoading(true);
    
    const newItems = await fetchItems(page);
    
    if (newItems.length === 0) {
      setHasMore(false);
    } else {
      setItems(prev => [...prev, ...newItems]);
      setPage(p => p + 1);
    }
    
    setLoading(false);
  };

  // Load first page on mount
  useEffect(() => { loadMore(); }, []);

  return (
    <div>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      
      {loading && <p style={{ textAlign: "center" }}>Loading...</p>}
      
      {hasMore && !loading && (
        <button 
          onClick={loadMore}
          style={{ display: "block", margin: "24px auto", padding: "12px 32px" }}
        >
          Load More
        </button>
      )}
      
      {!hasMore && items.length > 0 && (
        <p style={{ textAlign: "center", color: "#999" }}>
          You've reached the end — {items.length} items total
        </p>
      )}
    </div>
  );
}

14. Common Mistakes with Lists

Mistake 1: Missing Keys

// ❌ No key — React shows a warning
users.map(user => <li>{user.name}</li>);

// ✅ Always provide a key
users.map(user => <li key={user.id}>{user.name}</li>);

Mistake 2: Using Index as Key (When Data Changes)

// ❌ Problematic when items are reordered, added, or deleted
items.map((item, index) => <ListItem key={index} item={item} />);

// ✅ Use a stable, unique identifier
items.map(item => <ListItem key={item.id} item={item} />);

Mistake 3: Mutating the Array

// ❌ Mutates the original array
function SortedList({ items }) {
  items.sort((a, b) => a.name.localeCompare(b.name));  // MUTATES!
  return items.map(item => <li key={item.id}>{item.name}</li>);
}

// ✅ Copy, then sort
function SortedList({ items }) {
  const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
  return sorted.map(item => <li key={item.id}>{item.name}</li>);
}

Mistake 4: Not Handling Empty Arrays

// ❌ Renders an empty <ul> with no feedback
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

// ✅ Handle the empty case
function UserList({ users }) {
  if (users.length === 0) {
    return <p>No users found</p>;
  }
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Mistake 5: Forgetting return in map()

// ❌ No return — renders nothing (array of undefined)
users.map(user => {
  <li key={user.id}>{user.name}</li>;  // Missing return!
});

// ✅ Option A: Arrow function with parentheses (implicit return)
users.map(user => (
  <li key={user.id}>{user.name}</li>
));

// ✅ Option B: Explicit return
users.map(user => {
  return <li key={user.id}>{user.name}</li>;
});

15. Key Takeaways

  1. Array.map() is THE way to render lists in React — transforms data into JSX
  2. Always provide a key prop on the outermost element in map() — use unique IDs, not indices
  3. Never mutate the original array — use [...array].sort(), .filter(), .slice()
  4. Chain operations: .filter().sort().slice().map() for complex data transformations
  5. Extract list items into their own components when they're more than a few lines
  6. Handle empty states — never render an empty container without feedback
  7. Handle loading states — show skeletons or spinners while data loads
  8. Group data with .reduce() before rendering sectioned lists
  9. Paginate large lists — either page-based or "Load More" pattern
  10. Filter and sort before map() — never inside JSX expressions

Explain-It Challenge

  1. The Factory Assembly Line: A car factory has raw materials (steel, glass, rubber) that go through stations (cutting, shaping, painting) to become cars. Map this to React's .filter().sort().map() pipeline. What are the "raw materials"? What are the "stations"? What's the "finished product"?

  2. The Playlist: You have 1,000 songs in your music library. Explain how you'd build a playlist view that can filter by genre, sort by artist name, and show 20 songs at a time with a "Load More" button. Walk through the complete data flow.

  3. Why Keys?: Your friend says "My list renders fine without keys, so why bother?" Explain what happens internally when React doesn't have keys, and demonstrate a scenario where missing keys cause a visible bug (hint: think about stateful list items being reordered).


Navigation: ← 2.2.c — Dynamic Rendering · Next → 2.2.e — Keys in React