javascript-today

and more

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} />)
)}