javascript-today

Lists and Keys

Rendering Lists in React

Lists are one of the most common patterns in React. You’ll use .map() to transform arrays into JSX elements.

function FruitList() {
  const fruits = ['Apple', 'Banana', 'Orange'];
  
  return (
    <ul>
      {fruits.map(fruit => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
}

The .map() Method

.map() transforms each array item into JSX:

Basic List

function NumberList() {
  const numbers = [1, 2, 3, 4, 5];
  
  return (
    <ul>
      {numbers.map(number => (
        <li key={number}>Number: {number}</li>
      ))}
    </ul>
  );
}

List of Objects

function UserList() {
  const users = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
    { id: 3, name: 'Charlie', email: 'charlie@example.com' }
  ];
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
}

With Components

function ProductList({ products }) {
  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard 
          key={product.id}
          product={product}
        />
      ))}
    </div>
  );
}

function ProductCard({ product }) {
  return (
    <div className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

Keys - Why They Matter

Keys help React identify which items changed, were added, or were removed:

Without Keys (Bad)

// ❌ React will warn about missing keys
function List() {
  const items = ['A', 'B', 'C'];
  return (
    <ul>
      {items.map(item => <li>{item}</li>)}
    </ul>
  );
}
// Warning: Each child in a list should have a unique "key" prop

With Keys (Good)

// ✅ Keys help React track items
function List() {
  const items = [
    { id: 1, text: 'A' },
    { id: 2, text: 'B' },
    { id: 3, text: 'C' }
  ];
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

Why Keys Are Important

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Build app', done: false }
  ]);
  
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input 
            type="checkbox" 
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
// Without keys, React might update the wrong checkbox!

Choosing Good Keys

✅ Use Unique IDs

// Best - use database IDs
function UserList({ users }) {
  return users.map(user => (
    <UserCard key={user.id} user={user} />
  ));
}

// Good - use stable unique identifiers
function PostList({ posts }) {
  return posts.map(post => (
    <Post key={post.slug} post={post} />
  ));
}

⚠️ Index as Key (Only When Safe)

// OK - static list that never changes
function MonthList() {
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
  
  return months.map((month, index) => (
    <li key={index}>{month}</li>
  ));
}

// ❌ Bad - dynamic list with reordering/filtering
function TodoList({ todos }) {
  return todos.map((todo, index) => (
    <li key={index}>{todo.text}</li>
    // Bugs when todos are added/removed/reordered!
  ));
}

Generate IDs for Items Without Them

import { nanoid } from 'nanoid';  // or use crypto.randomUUID()

function TodoApp() {
  const [todos, setTodos] = useState([]);
  
  function addTodo(text) {
    const newTodo = {
      id: nanoid(),  // Generate unique ID
      text,
      done: false
    };
    setTodos([...todos, newTodo]);
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

Filtering Lists

Show a subset of items based on criteria:

Basic Filter

function ProductList({ products, showOnSale }) {
  const displayProducts = showOnSale
    ? products.filter(p => p.onSale)
    : products;
  
  return (
    <div>
      {displayProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Search Filter

function UserList() {
  const [searchTerm, setSearchTerm] = useState('');
  const users = [
    { id: 1, name: 'Alice Johnson' },
    { id: 2, name: 'Bob Smith' },
    { id: 3, name: 'Charlie Brown' }
  ];
  
  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Multiple Filters

function ProductList({ products }) {
  const [filters, setFilters] = useState({
    category: 'all',
    minPrice: 0,
    maxPrice: Infinity,
    inStock: false
  });
  
  const filteredProducts = products
    .filter(p => filters.category === 'all' || p.category === filters.category)
    .filter(p => p.price >= filters.minPrice)
    .filter(p => p.price <= filters.maxPrice)
    .filter(p => !filters.inStock || p.stock > 0);
  
  return (
    <div>
      {/* Filter controls */}
      <div>
        <select 
          value={filters.category}
          onChange={(e) => setFilters({ ...filters, category: e.target.value })}
        >
          <option value="all">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
        </select>
        
        <label>
          <input
            type="checkbox"
            checked={filters.inStock}
            onChange={(e) => setFilters({ ...filters, inStock: e.target.checked })}
          />
          In Stock Only
        </label>
      </div>
      
      {/* Product list */}
      <div>
        {filteredProducts.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

Sorting Lists

Reorder items based on criteria:

Basic Sort

function UserList({ users }) {
  const [sortBy, setSortBy] = useState('name');
  
  const sortedUsers = [...users].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    }
    if (sortBy === 'age') {
      return a.age - b.age;
    }
    return 0;
  });
  
  return (
    <div>
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">Sort by Name</option>
        <option value="age">Sort by Age</option>
      </select>
      
      <ul>
        {sortedUsers.map(user => (
          <li key={user.id}>
            {user.name} ({user.age})
          </li>
        ))}
      </ul>
    </div>
  );
}

Sort Direction

function ProductList({ products }) {
  const [sortField, setSortField] = useState('name');
  const [sortDirection, setSortDirection] = useState('asc');
  
  const sortedProducts = [...products].sort((a, b) => {
    const aVal = a[sortField];
    const bVal = b[sortField];
    
    let comparison = 0;
    if (typeof aVal === 'string') {
      comparison = aVal.localeCompare(bVal);
    } else {
      comparison = aVal - bVal;
    }
    
    return sortDirection === 'asc' ? comparison : -comparison;
  });
  
  return (
    <div>
      <select value={sortField} onChange={(e) => setSortField(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
        <option value="rating">Rating</option>
      </select>
      
      <button onClick={() => setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')}>
        {sortDirection === 'asc' ? '↑' : '↓'}
      </button>
      
      {sortedProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Combining Filter and Sort

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', priority: 'high', done: false },
    { id: 2, text: 'Build app', priority: 'medium', done: false },
    { id: 3, text: 'Deploy', priority: 'low', done: true }
  ]);
  
  const [filter, setFilter] = useState('all');  // all, active, done
  const [sortBy, setSortBy] = useState('priority');
  
  const processedTodos = todos
    // Filter first
    .filter(todo => {
      if (filter === 'active') return !todo.done;
      if (filter === 'done') return todo.done;
      return true;
    })
    // Then sort
    .sort((a, b) => {
      if (sortBy === 'priority') {
        const priorityOrder = { high: 1, medium: 2, low: 3 };
        return priorityOrder[a.priority] - priorityOrder[b.priority];
      }
      return 0;
    });
  
  return (
    <div>
      <div>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('done')}>Done</button>
      </div>
      
      <ul>
        {processedTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => {/* toggle */}}
            />
            {todo.text} ({todo.priority})
          </li>
        ))}
      </ul>
    </div>
  );
}

Nested Lists

Lists inside lists:

Simple Nested List

function CategoryList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

Nested Components

function CommentThread({ comments }) {
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id} className="comment">
          <p>{comment.text}</p>
          <span>{comment.author}</span>
          
          {comment.replies && comment.replies.length > 0 && (
            <div className="replies">
              {comment.replies.map(reply => (
                <div key={reply.id} className="reply">
                  <p>{reply.text}</p>
                  <span>{reply.author}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

Empty States

Handle empty lists gracefully:

function TodoList({ todos }) {
  if (todos.length === 0) {
    return (
      <div className="empty-state">
        <p>No todos yet!</p>
        <button>Add your first todo</button>
      </div>
    );
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

// Or inline
function TodoList({ todos }) {
  return (
    <div>
      {todos.length === 0 ? (
        <p>No todos yet!</p>
      ) : (
        <ul>
          {todos.map(todo => (
            <li key={todo.id}>{todo.text}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Common Patterns

1. Add/Remove from List

function ShoppingList() {
  const [items, setItems] = useState([]);
  const [input, setInput] = useState('');
  
  function addItem() {
    if (input.trim()) {
      setItems([...items, { id: Date.now(), text: input }]);
      setInput('');
    }
  }
  
  function removeItem(id) {
    setItems(items.filter(item => item.id !== id));
  }
  
  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && addItem()}
      />
      <button onClick={addItem}>Add</button>
      
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.text}
            <button onClick={() => removeItem(item.id)}></button>
          </li>
        ))}
      </ul>
    </div>
  );
}

2. Update Item in List

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Build app', done: false }
  ]);
  
  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  }
  
  function editTodo(id, newText) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, text: newText } : todo
    ));
  }
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

3. Pagination

function UserList({ users }) {
  const [page, setPage] = useState(1);
  const perPage = 10;
  
  const startIndex = (page - 1) * perPage;
  const endIndex = startIndex + perPage;
  const pageUsers = users.slice(startIndex, endIndex);
  const totalPages = Math.ceil(users.length / perPage);
  
  return (
    <div>
      <ul>
        {pageUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      
      <div>
        <button 
          onClick={() => setPage(page - 1)}
          disabled={page === 1}
        >
          Previous
        </button>
        <span>Page {page} of {totalPages}</span>
        <button 
          onClick={() => setPage(page + 1)}
          disabled={page === totalPages}
        >
          Next
        </button>
      </div>
    </div>
  );
}

Best Practices

DO:

// Use unique, stable IDs as keys
<li key={item.id}>{item.name}</li>

// Filter/sort before mapping
const filtered = items.filter(i => i.active);
return filtered.map(item => <Item key={item.id} />);

// Extract list items to components
{users.map(user => <UserCard key={user.id} user={user} />)}

// Handle empty lists
{items.length === 0 ? <EmptyState /> : <ItemList items={items} />}

// Use [...array] when sorting (don't mutate)
const sorted = [...items].sort((a, b) => a.value - b.value);

DON’T:

// Don't use index as key for dynamic lists
{todos.map((todo, index) => <li key={index}>{todo.text}</li>)}

// Don't mutate the array
items.sort();  // ❌ Mutates original
return items.map(i => <Item key={i.id} />);

// Don't forget keys
{items.map(item => <li>{item.text}</li>)}  // Warning!

// Don't use random values as keys
{items.map(item => <li key={Math.random()}>{item.text}</li>)}

// Don't use non-unique keys
{items.map(item => <li key={item.category}>{item.name}</li>)}

Summary

Basic List Rendering:

{items.map(item => (
  <div key={item.id}>{item.name}</div>
))}

Keys:

  • Required for list items
  • Must be unique among siblings
  • Should be stable (don’t use Math.random())
  • Best: Use IDs from data
  • OK: Use index only for static lists

Common Operations:

// Filter
const filtered = items.filter(i => i.active);

// Sort (don't mutate!)
const sorted = [...items].sort((a, b) => a.value - b.value);

// Add
setItems([...items, newItem]);

// Remove
setItems(items.filter(i => i.id !== removeId));

// Update
setItems(items.map(i => i.id === updateId ? { ...i, done: true } : i));

Empty State:

{items.length === 0 ? (
  <EmptyMessage />
) : (
  items.map(item => <Item key={item.id} />)
)}