Skip to main content

📜 Lists and Keys

Real applications work with collections of data - users, products, posts, comments, messages, and more. Rendering lists efficiently is a fundamental React skill. But there's a catch: React needs a special "key" prop to track items in lists. Get it wrong, and you'll have bugs that are hard to debug. Get it right, and your lists will be fast, predictable, and bug-free. In this lesson, you'll master rendering lists, understand why keys matter, and learn how to perform CRUD operations on dynamic lists. Let's make lists awesome! 🚀

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Render arrays of data using map()
  • Understand why the key prop is critical
  • Choose appropriate keys for different scenarios
  • Implement CRUD operations on lists
  • Use filter(), reduce(), and other array methods
  • Type arrays properly with TypeScript
  • Handle empty lists gracefully
  • Optimize list rendering performance
  • Build dynamic, interactive lists

Estimated Time: 70-85 minutes

Project: Build an enhanced todo list with filtering, sorting, and search

📑 In This Lesson

📋 Rendering Lists

In React, you render lists by transforming arrays of data into arrays of JSX elements using JavaScript's map() function.

Basic List Rendering

Simple Array to JSX

const SimpleList: React.FC = () => {
    const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];
    
    return (
        <ul>
            {fruits.map((fruit, index) => (
                <li key={index}>{fruit}</li>
            ))}
        </ul>
    );
};

// Renders:
// • Apple
// • Banana
// • Cherry
// • Date

How map() Works

Understanding the Transformation

// What we start with
const numbers = [1, 2, 3];

// map() transforms each item
const doubled = numbers.map(num => num * 2);
// [2, 4, 6]

// In React, we transform data into JSX
const listItems = numbers.map(num => <li>{num}</li>);
// [<li>1</li>, <li>2</li>, <li>3</li>]

// JSX expression:
<ul>{listItems}</ul>

Rendering Objects

Lists of Objects

interface User {
    id: number;
    name: string;
    email: string;
}

const UserList: React.FC = () => {
    const users: User[] = [
        { id: 1, name: 'Alice', email: '[email protected]' },
        { id: 2, name: 'Bob', email: '[email protected]' },
        { id: 3, name: 'Charlie', email: '[email protected]' }
    ];
    
    return (
        <div>
            {users.map(user => (
                <div key={user.id} style={{ padding: '1rem', border: '1px solid #ddd', marginBottom: '0.5rem' }}>
                    <h3>{user.name}</h3>
                    <p>{user.email}</p>
                </div>
            ))}
        </div>
    );
};

Common Pattern: Extract to Component

Keep It Clean

interface User {
    id: number;
    name: string;
    email: string;
}

// Individual item component
interface UserCardProps {
    user: User;
}

const UserCard: React.FC<UserCardProps> = ({ user }) => (
    <div style={{ padding: '1rem', border: '1px solid #ddd', marginBottom: '0.5rem' }}>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
    </div>
);

// List component
const UserList: React.FC = () => {
    const users: User[] = [
        { id: 1, name: 'Alice', email: '[email protected]' },
        { id: 2, name: 'Bob', email: '[email protected]' },
        { id: 3, name: 'Charlie', email: '[email protected]' }
    ];
    
    return (
        <div>
            {users.map(user => (
                <UserCard key={user.id} user={user} />
            ))}
        </div>
    );
};

✅ This is cleaner and more maintainable!

💡 Key Insight

The pattern is always the same:

  1. Have an array of data
  2. Use .map() to transform each item
  3. Return JSX for each item
  4. Add a unique key prop

🔑 The Key Prop

The key prop is one of React's most misunderstood features. Let's demystify it!

📖 Definition

Key Prop: A special attribute that helps React identify which items have changed, been added, or been removed. Keys give list items a stable identity.

Why Keys Matter

What Happens Without Keys?

// ❌ Bad: No keys
<ul>
    {items.map(item => <li>{item}</li>)}
</ul>

// React console warning:
// Warning: Each child in a list should have a unique "key" prop.

Without keys, React doesn't know which element is which. This causes:

  • ⚠️ React warns in console
  • ⚠️ Potential rendering bugs
  • ⚠️ Poor performance on updates
  • ⚠️ State can get mixed up

How Keys Work

graph TD
    A[Array Changes] --> B{React compares keys}
    B -->|Same key| C[Update element]
    B -->|New key| D[Create new element]
    B -->|Key removed| E[Delete element]
    B -->|Key moved| F[Move element]
    
    style C fill:#4CAF50,color:#fff
    style D fill:#2196F3,color:#fff
    style E fill:#f44336,color:#fff
    style F fill:#FF9800,color:#fff

Keys Must Be Unique Among Siblings

Uniqueness Rules

// ✅ Good: Unique keys among siblings
<ul>
    {users.map(user => (
        <li key={user.id}>{user.name}</li>
    ))}
</ul>

// ✅ Also good: Different lists can reuse keys
<div>
    <ul>
        {users.map(user => <li key={user.id}>{user.name}</li>)}

                

🎨 How Keys Help React Track Items

Keys Tell React Which Items Changed ✅ With Stable Keys (IDs) Before: ["A", "B", "C"] key="a" → A key="b" → B key="c" → C Insert "D" at position 1 After: ["A", "D", "B", "C"] key="a" → A Reused ✓ key="d" → D NEW! ✓ key="b" → B Moved ✓ key="c" → C Moved ✓ Result: React understands! • Creates 1 new element (D) • Reuses A, moves B and C ⚠️ With Index as Key Before: ["A", "B", "C"] key=0 → A key=1 → B key=2 → C Insert "D" at position 1 After: ["A", "D", "B", "C"] key=0 → A OK ✓ key=1 → D Was B! ✗ key=2 → B Was C! ✗ key=3 → C NEW! ✗ Result: React is confused! • Updates 3 elements (B→D, C→B, new C) • State in B,C components may be wrong! </ul> <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> </div> // ❌ Bad: Duplicate keys in same list <ul> <li key="1">Item 1</li> <li key="1">Item 2</li> {/* Duplicate key! */} </ul>

The Index as Key Problem

⚠️ Warning: Using Index as Key

// ⚠️ Problematic in many cases
{items.map((item, index) => (
    <li key={index}>{item}</li>
))}

Why it's problematic:

Imagine you have a list: ["A", "B", "C"]

key=0: A
key=1: B
key=2: C

You remove "A". Now you have: ["B", "C"]

key=0: B  (was key=1 before!)
key=1: C  (was key=2 before!)

React thinks items at key=0 and key=1 changed content, when really you just deleted one item!

When index as key is okay:

  • ✅ List is static (never changes)
  • ✅ Items are never reordered
  • ✅ Items are never deleted
  • ✅ Items have no state or controlled inputs

When NOT to use index:

  • ❌ Items can be added/removed
  • ❌ List can be sorted/filtered
  • ❌ Items have input fields
  • ❌ Items have local component state

Example: The Bug Index Keys Can Cause

Demonstration of the Problem

interface TodoItemProps {
    text: string;
}

const TodoItem: React.FC<TodoItemProps> = ({ text }) => {
    const [done, setDone] = useState(false);
    
    return (
        <div>
            <input
                type="checkbox"
                checked={done}
                onChange={() => setDone(!done)}
            />
            {text}
        </div>
    );
};

// ❌ Using index as key
const BadTodoList: React.FC = () => {
    const [todos, setTodos] = useState(['Task A', 'Task B', 'Task C']);
    
    return (
        <div>
            {todos.map((todo, index) => (
                <TodoItem key={index} text={todo} />  {/* Bug! */}
            ))}
            <button onClick={() => setTodos(todos.slice(1))}>
                Remove first item
            </button>
        </div>
    );
};

// What happens:
// 1. Check "Task A"
// 2. Remove first item
// 3. Now "Task B" appears checked! (because it has the old key=0)

✅ The Fix: Use Stable IDs

interface Todo {
    id: number;  // or string
    text: string;
}

const GoodTodoList: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([
        { id: 1, text: 'Task A' },
        { id: 2, text: 'Task B' },
        { id: 3, text: 'Task C' }
    ]);
    
    return (
        <div>
            {todos.map(todo => (
                <TodoItem key={todo.id} text={todo.text} />  {/* ✅ Correct! */}
            ))}
        </div>
    );
};

🎯 Choosing Good Keys

A good key is stable, unique, and consistent. Let's learn how to choose keys properly.

Best Key Sources

Source When to Use Example
Database ID Data from backend with IDs key={user.id}
UUID/GUID Generated unique IDs key={item.uuid}
Unique Property Natural unique identifier key={user.email}
Generated on Creation Client-side items key={Date.now()}
Composite Key Multiple properties unique key={`${cat}-${id}`}
Index Static, never-changing list key={index}

Generating Keys for New Items

Pattern: Add ID When Creating

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const TodoApp: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    
    const addTodo = (text: string) => {
        const newTodo: Todo = {
            id: Date.now(),  // Simple unique ID
            text: text,
            completed: false
        };
        setTodos([...todos, newTodo]);
    };
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
};

Better ID Generation

Using Libraries or Crypto

// Option 1: Use crypto.randomUUID() (modern browsers)
const id = crypto.randomUUID();
// "36b8f84d-df4e-4d49-b662-bcde71a8764f"

// Option 2: Use nanoid library
import { nanoid } from 'nanoid';
const id = nanoid();
// "V1StGXR8_Z5jdHi6B-myT"

// Option 3: Simple counter (if single user)
let nextId = 1;
const generateId = () => nextId++;

const addTodo = (text: string) => {
    const newTodo = {
        id: generateId(),  // or crypto.randomUUID()
        text,
        completed: false
    };
    setTodos([...todos, newTodo]);
};

✅ Key Guidelines

  • Stable: Same item always has same key
  • Unique: No two siblings share a key
  • Consistent: Key doesn't change between renders
  • Not Random: Don't use Math.random() as keys!

⚠️ Common Mistakes

// ❌ Bad: Random key (changes every render!)
key={Math.random()}

// ❌ Bad: Non-unique key
key="same-key-for-all"

// ❌ Bad: Duplicate keys
items.map(item => <li key={1}>...</li>)

// ❌ Bad: Complex object as key (must be string/number)
key={{id: 1}}

🎨 Keys Quick Reference

Choosing the Right Key ✅ Good Keys Database IDs: key={user.id} UUIDs: key={crypto.randomUUID()} // on creation Unique Properties: key={email} // if guaranteed unique Composite Keys: key={`${category}-${id}`} ❌ Bad Keys Index (for dynamic lists): key={index} // changes when list changes! Random Values: key={Math.random()} // new key every render! Same Key for All: key="item" // duplicates cause bugs! Objects as Keys: key={{id: 1}} // must be string/number!

🔧 Array Methods (map, filter, reduce)

JavaScript's array methods are essential for working with lists in React. Let's master the most important ones!

map() - Transform Each Item

Converting Data to JSX

// map() creates a new array by transforming each element
const numbers = [1, 2, 3, 4, 5];

// Example 1: Double each number
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]

// Example 2: Convert to JSX
const listItems = numbers.map(n => <li key={n}>{n}</li>);
// [<li>1</li>, <li>2</li>, ...]

// Example 3: Extract properties
interface User {
    id: number;
    name: string;
    email: string;
}

const users: User[] = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' }
];

const names = users.map(user => user.name);
// ['Alice', 'Bob']

const userCards = users.map(user => (
    <div key={user.id}>
        <h3>{user.name}</h3>
        <p>{user.email}</p>
    </div>
));

filter() - Select Items

Filtering Lists

// filter() creates a new array with items that pass a test
const numbers = [1, 2, 3, 4, 5, 6];

// Example 1: Get even numbers
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4, 6]

// Example 2: Filter completed todos
interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const todos: Todo[] = [
    { id: 1, text: 'Learn React', completed: true },
    { id: 2, text: 'Build app', completed: false },
    { id: 3, text: 'Deploy', completed: false }
];

const completed = todos.filter(todo => todo.completed);
// [{ id: 1, text: 'Learn React', completed: true }]

const active = todos.filter(todo => !todo.completed);
// [{ id: 2, ... }, { id: 3, ... }]

// Example 3: Search functionality
const searchTerm = 'react';
const searchResults = todos.filter(todo => 
    todo.text.toLowerCase().includes(searchTerm.toLowerCase())
);
// [{ id: 1, text: 'Learn React', completed: true }]

reduce() - Compute Single Value

Aggregating Data

// reduce() combines all elements into a single value
const numbers = [1, 2, 3, 4, 5];

// Example 1: Sum
const sum = numbers.reduce((total, num) => total + num, 0);
// 15

// Example 2: Count completed todos
const completedCount = todos.reduce((count, todo) => 
    count + (todo.completed ? 1 : 0), 0
);

// Example 3: Calculate total price
interface CartItem {
    id: number;
    name: string;
    price: number;
    quantity: number;
}

const cart: CartItem[] = [
    { id: 1, name: 'Book', price: 12.99, quantity: 2 },
    { id: 2, name: 'Pen', price: 1.99, quantity: 5 }
];

const total = cart.reduce((sum, item) => 
    sum + (item.price * item.quantity), 0
);
// 35.93

// Example 4: Group by property
const grouped = todos.reduce((groups, todo) => {
    const key = todo.completed ? 'completed' : 'active';
    if (!groups[key]) groups[key] = [];
    groups[key].push(todo);
    return groups;
}, {} as Record<string, Todo[]>);
// { completed: [...], active: [...] }

Chaining Array Methods

Combining Operations

// You can chain methods for powerful transformations
const todos: Todo[] = [
    { id: 1, text: 'Learn React', completed: true },
    { id: 2, text: 'Build App', completed: false },
    { id: 3, text: 'Learn TypeScript', completed: true },
    { id: 4, text: 'Deploy', completed: false }
];

// Example 1: Filter then map
const completedTexts = todos
    .filter(todo => todo.completed)
    .map(todo => todo.text);
// ['Learn React', 'Learn TypeScript']

// Example 2: Filter, map, then render
const ActiveTodoList: React.FC = () => {
    return (
        <ul>
            {todos
                .filter(todo => !todo.completed)
                .map(todo => (
                    <li key={todo.id}>{todo.text}</li>
                ))
            }
        </ul>
    );
};

// Example 3: Search, filter, sort, map
const searchTerm = 'learn';

const searchResults = todos
    .filter(todo => todo.text.toLowerCase().includes(searchTerm.toLowerCase()))
    .sort((a, b) => a.text.localeCompare(b.text))
    .map(todo => (
        <div key={todo.id}>
            <strong>{todo.text}</strong>
            <span>{todo.completed ? ' ✓' : ''}</span>
        </div>
    ));

Other Useful Array Methods

Method Purpose Example
find() Get first matching item todos.find(t => t.id === 2)
findIndex() Get index of first match todos.findIndex(t => t.id === 2)
some() Check if any item matches todos.some(t => t.completed)
every() Check if all items match todos.every(t => t.completed)
slice() Get portion of array todos.slice(0, 5)
sort() Sort array (copy first!) [...todos].sort((a,b) => ...)

✅ Array Methods Cheat Sheet

  • map(): Transform each item → new array
  • filter(): Select items that match → new array
  • reduce(): Combine all items → single value
  • find(): Get first match → single item or undefined
  • some(): Any item matches? → boolean
  • every(): All items match? → boolean

🎮 Interactive Demo: Array Methods in Action

Watch how map, filter, and reduce transform arrays:

✏️ CRUD Operations

Let's implement Create, Read, Update, and Delete operations on lists!

Complete CRUD Example

Full Todo App with CRUD

import React, { useState } from 'react';

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const TodoApp: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([
        { id: 1, text: 'Learn React', completed: false },
        { id: 2, text: 'Build App', completed: false }
    ]);
    const [inputText, setInputText] = useState('');

    // CREATE - Add new todo
    const addTodo = () => {
        if (inputText.trim() === '') return;
        
        const newTodo: Todo = {
            id: Date.now(),
            text: inputText,
            completed: false
        };
        
        setTodos([...todos, newTodo]);
        setInputText('');
    };

    // READ - Already handled by rendering the list
    
    // UPDATE - Toggle completion
    const toggleTodo = (id: number) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };

    // UPDATE - Edit text
    const updateTodoText = (id: number, newText: string) => {
        setTodos(todos.map(todo =>
            todo.id === id
                ? { ...todo, text: newText }
                : todo
        ));
    };

    // DELETE - Remove todo
    const deleteTodo = (id: number) => {
        setTodos(todos.filter(todo => todo.id !== id));
    };

    // DELETE - Clear completed
    const clearCompleted = () => {
        setTodos(todos.filter(todo => !todo.completed));
    };

    return (
        <div style={{ maxWidth: '600px', margin: '0 auto', padding: '2rem' }}>
            <h1>Todo List</h1>
            
            {/* CREATE */}
            <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
                <input
                    value={inputText}
                    onChange={(e) => setInputText(e.target.value)}
                    onKeyPress={(e) => e.key === 'Enter' && addTodo()}
                    placeholder="Add a todo..."
                    style={{ flex: 1, padding: '0.5rem' }}
                />
                <button onClick={addTodo} style={{ padding: '0.5rem 1rem' }}>
                    Add
                </button>
            </div>

            {/* READ - Display list */}
            <ul style={{ listStyle: 'none', padding: 0 }}>
                {todos.map(todo => (
                    <li
                        key={todo.id}
                        style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: '0.5rem',
                            padding: '0.5rem',
                            background: '#f5f5f5',
                            marginBottom: '0.5rem',
                            borderRadius: '4px'
                        }}
                    >
                        {/* UPDATE - Toggle */}
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(todo.id)}
                        />
                        
                        <span
                            style={{
                                flex: 1,
                                textDecoration: todo.completed ? 'line-through' : 'none',
                                color: todo.completed ? '#999' : '#000'
                            }}
                        >
                            {todo.text}
                        </span>
                        

                

🎮 Interactive Demo: CRUD in Action

See Create, Read, Update, Delete operations on a live list:

{/* DELETE */} <button onClick={() => deleteTodo(todo.id)} style={{ background: '#f44336', color: 'white', border: 'none', padding: '0.25rem 0.5rem', borderRadius: '4px', cursor: 'pointer' }} > Delete </button> </li> ))} </ul> {/* Additional actions */} <div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'space-between' }}> <span>{todos.filter(t => !t.completed).length} items left</span> <button onClick={clearCompleted}>Clear completed</button> </div> </div> ); }; export default TodoApp;

CRUD Operation Patterns

Operation Pattern Example
Create (Add) Spread and append [...items, newItem]
Read (Display) Map to JSX items.map(item => <li>...</li>)
Update Map and replace items.map(i => i.id === id ? updated : i)
Delete Filter out items.filter(i => i.id !== id)

Bulk Operations

Operating on Multiple Items

// Select/deselect all
const [todos, setTodos] = useState<Todo[]>([]);

const toggleAll = () => {
    const allCompleted = todos.every(todo => todo.completed);
    setTodos(todos.map(todo => ({
        ...todo,
        completed: !allCompleted
    })));
};

// Delete multiple
const deleteMultiple = (idsToDelete: number[]) => {
    setTodos(todos.filter(todo => !idsToDelete.includes(todo.id)));
};

// Update multiple
const markSelectedAsCompleted = (selectedIds: number[]) => {
    setTodos(todos.map(todo =>
        selectedIds.includes(todo.id)
            ? { ...todo, completed: true }
            : todo
    ));
};

Optimistic Updates

Update UI Immediately

const deleteTodoWithAPI = async (id: number) => {
    // 1. Update UI immediately (optimistic)
    const previousTodos = todos;
    setTodos(todos.filter(todo => todo.id !== id));
    
    try {
        // 2. Send request to server
        await api.deleteTodo(id);
        // Success! UI is already updated
    } catch (error) {
        // 3. If error, revert UI
        setTodos(previousTodos);
        alert('Failed to delete todo');
    }
};

📘 Typing Lists with TypeScript

TypeScript makes working with lists safer and more predictable. Let's type everything properly!

Array Type Annotations

Different Ways to Type Arrays

// Method 1: Type[]
const numbers: number[] = [1, 2, 3];
const strings: string[] = ['a', 'b', 'c'];

// Method 2: Array<Type>
const numbers: Array<number> = [1, 2, 3];
const strings: Array<string> = ['a', 'b', 'c'];

// Interface arrays
interface User {
    id: number;
    name: string;
}

const users: User[] = [];
// or
const users: Array<User> = [];

// Union type arrays
type Status = 'pending' | 'active' | 'completed';
const statuses: Status[] = ['pending', 'active'];

// Array of objects inline
const items: Array<{ id: number; name: string }> = [
    { id: 1, name: 'Item 1' }
];

Typing Props with Lists

List Component Types

interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

// List component props
interface TodoListProps {
    todos: Todo[];
    onToggle: (id: number) => void;
    onDelete: (id: number) => void;
}

const TodoList: React.FC<TodoListProps> = ({ todos, onToggle, onDelete }) => {
    return (
        <ul>
            {todos.map(todo => (
                <TodoItem
                    key={todo.id}
                    todo={todo}
                    onToggle={onToggle}
                    onDelete={onDelete}
                />
            ))}
        </ul>
    );
};

// Item component props
interface TodoItemProps {
    todo: Todo;
    onToggle: (id: number) => void;
    onDelete: (id: number) => void;
}

const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
    return (
        <li>
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => onDelete(todo.id)}>Delete</button>
        </li>
    );
};

Typing Array Methods

Type-Safe Transformations

interface Product {
    id: number;
    name: string;
    price: number;
    category: string;
}

const products: Product[] = [
    { id: 1, name: 'Book', price: 12.99, category: 'books' },
    { id: 2, name: 'Pen', price: 1.99, category: 'stationery' }
];

// TypeScript infers types in callbacks
const names = products.map(product => product.name);
// Type: string[]

const expensive = products.filter(product => product.price > 10);
// Type: Product[]

const total = products.reduce((sum, product) => sum + product.price, 0);
// Type: number

// Explicit typing when needed
const formatted = products.map((product): string => {
    return `${product.name}: $${product.price}`;
});
// Type: string[]

Generic List Components

Reusable Type-Safe Lists

// Generic list component
interface ListProps<T> {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
    getKey: (item: T) => string | number;
}

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return (
        <ul>
            {items.map(item => (
                <li key={getKey(item)}>
                    {renderItem(item)}
                </li>
            ))}
        </ul>
    );
}

// Usage with User type
interface User {
    id: number;
    name: string;
    email: string;
}

const users: User[] = [
    { id: 1, name: 'Alice', email: '[email protected]' },
    { id: 2, name: 'Bob', email: '[email protected]' }
];

const UserList: React.FC = () => (
    <List
        items={users}
        renderItem={(user) => (
            <div>
                <strong>{user.name}</strong>
                <p>{user.email}</p>
            </div>
        )}
        getKey={(user) => user.id}
    />
);

// Usage with Product type
interface Product {
    id: string;
    name: string;
    price: number;
}

const products: Product[] = [
    { id: 'p1', name: 'Book', price: 12.99 }
];

const ProductList: React.FC = () => (
    <List
        items={products}
        renderItem={(product) => (
            <span>{product.name}: ${product.price}</span>
        )}
        getKey={(product) => product.id}
    />
);

💡 TypeScript Benefits for Lists

  • Autocomplete: IDE suggests available properties
  • Type safety: Catches errors at compile time
  • Refactor confidence: Rename properties safely
  • Documentation: Types document what data looks like
  • Inference: Often don't need explicit types in callbacks

🗂️ Empty States and Conditional Lists

Handling empty lists gracefully is important for good UX. Let's learn proper empty state patterns.

Checking for Empty Lists

Basic Empty State

const TodoList: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    
    // Check if empty
    if (todos.length === 0) {
        return (
            <div style={{ textAlign: 'center', padding: '3rem' }}>
                <p style={{ fontSize: '3rem' }}>📝</p>
                <h3>No todos yet</h3>
                <p>Add your first todo to get started!</p>
            </div>
        );
    }
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id}>{todo.text}</li>
            ))}
        </ul>
    );
};

Conditional Rendering Patterns

Different Empty State Approaches

// Pattern 1: Ternary operator
<div>
    {todos.length === 0 ? (
        <p>No todos yet!</p>
    ) : (
        <ul>
            {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
        </ul>
    )}
</div>

// Pattern 2: Logical && operator
<div>
    {todos.length === 0 && <p>No todos yet!</p>}
    {todos.length > 0 && (
        <ul>
            {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
        </ul>
    )}
</div>

// Pattern 3: Early return (component level)
const TodoList: React.FC<{ todos: Todo[] }> = ({ todos }) => {
    if (todos.length === 0) {
        return <EmptyState />;
    }
    
    return (
        <ul>
            {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
        </ul>
    );
};

Rich Empty States

Engaging Empty State Component

interface EmptyStateProps {
    icon?: string;
    title: string;
    description: string;
    action?: {
        label: string;
        onClick: () => void;
    };
}

const EmptyState: React.FC<EmptyStateProps> = ({ 
    icon = '📭', 
    title, 
    description, 
    action 
}) => {
    return (
        <div style={{
            textAlign: 'center',
            padding: '3rem',
            background: '#f5f5f5',
            borderRadius: '8px',
            margin: '2rem 0'
        }}>
            <div style={{ fontSize: '4rem', marginBottom: '1rem' }}>
                {icon}
            </div>
            <h3 style={{ marginBottom: '0.5rem' }}>{title}</h3>
            <p style={{ color: '#666', marginBottom: '1rem' }}>{description}</p>
            {action && (
                <button
                    onClick={action.onClick}
                    style={{
                        padding: '0.75rem 1.5rem',
                        background: '#667eea',
                        color: 'white',
                        border: 'none',
                        borderRadius: '4px',
                        cursor: 'pointer',
                        fontSize: '1rem'
                    }}
                >
                    {action.label}
                </button>
            )}
        </div>
    );
};

// Usage
const MyTodos: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [showAddForm, setShowAddForm] = useState(false);
    
    if (todos.length === 0) {
        return (
            <EmptyState
                icon="✨"
                title="No todos yet"
                description="Get started by adding your first todo item"
                action={{
                    label: 'Add Todo',
                    onClick: () => setShowAddForm(true)
                }}
            />
        );
    }
    
    return <TodoList todos={todos} />;
};

Filtered Empty States

Different Empty Messages

const FilteredTodoList: React.FC = () => {
    const [todos, setTodos] = useState<Todo[]>([]);
    const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
    const [searchTerm, setSearchTerm] = useState('');
    
    // Apply filters
    const filteredTodos = todos
        .filter(todo => {
            if (filter === 'active') return !todo.completed;
            if (filter === 'completed') return todo.completed;
            return true;
        })
        .filter(todo => 
            todo.text.toLowerCase().includes(searchTerm.toLowerCase())
        );
    
    // Different messages based on context
    const getEmptyMessage = () => {
        if (todos.length === 0) {
            return 'No todos yet. Add one to get started!';
        }
        if (searchTerm) {
            return `No todos found matching "${searchTerm}"`;
        }
        if (filter === 'completed') {
            return 'No completed todos yet. Keep working!';
        }
        if (filter === 'active') {
            return 'All done! No active todos.';
        }
        return 'No todos found.';
    };
    
    return (
        <div>
            {filteredTodos.length === 0 ? (
                <p style={{ textAlign: 'center', color: '#666', padding: '2rem' }}>
                    {getEmptyMessage()}
                </p>
            ) : (
                <ul>
                    {filteredTodos.map(todo => (
                        <li key={todo.id}>{todo.text}</li>
                    ))}
                </ul>
            )}
        </div>
    );
};

✅ Empty State Best Practices

  • Always handle empty lists - Don't show blank space
  • Be contextual - Different messages for different situations
  • Provide guidance - Tell users what they can do
  • Add visual interest - Use icons or illustrations
  • Include actions - Make it easy to add first item

⚡ List Performance

For small lists (< 100 items), performance is rarely an issue. But let's learn optimization techniques for larger lists!

Why Keys Matter for Performance

React's Reconciliation Process

When your list changes, React uses keys to:

  • Determine which elements changed
  • Reuse existing DOM nodes when possible
  • Only update what actually changed

With good keys:

// List before: [A, B, C]
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>

// Add D at position 1: [A, D, B, C]
<li key="a">A</li>
<li key="d">D</li>  // ✅ New element created
<li key="b">B</li>  // ✅ Reused, just moved
<li key="c">C</li>  // ✅ Reused, just moved

With index as keys:

// List before: [A, B, C]
<li key="0">A</li>
<li key="1">B</li>
<li key="2">C</li>

// Add D at position 1: [A, D, B, C]
<li key="0">A</li>  // ✅ Reused
<li key="1">D</li>  // ⚠️ Updated (was B)
<li key="2">B</li>  // ⚠️ Updated (was C)
<li key="3">C</li>  // ⚠️ New element

Virtual Scrolling for Long Lists

Only Render Visible Items

For very long lists (1000+ items), use virtual scrolling libraries:

// Using react-window library
import { FixedSizeList } from 'react-window';

interface RowProps {
    index: number;
    style: React.CSSProperties;
}

const LargeList: React.FC = () => {
    const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
    
    const Row: React.FC<RowProps> = ({ index, style }) => (
        <div style={style}>
            {items[index]}
        </div>
    );
    
    return (
        <FixedSizeList
            height={400}
            itemCount={items.length}
            itemSize={35}
            width="100%"
        >
            {Row}
        </FixedSizeList>
    );
};

This renders only the visible items, dramatically improving performance!

Memoization with React.memo

Prevent Unnecessary Re-renders

interface TodoItemProps {
    todo: Todo;
    onToggle: (id: number) => void;
    onDelete: (id: number) => void;
}

// Without memo: Re-renders even if props haven't changed
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
    console.log('TodoItem rendered:', todo.id);
    return (
        <li>
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
            />
            {todo.text}
            <button onClick={() => onDelete(todo.id)}>Delete</button>
        </li>
    );
};

// With memo: Only re-renders if props change
const TodoItem = React.memo<TodoItemProps>(({ todo, onToggle, onDelete }) => {
    console.log('TodoItem rendered:', todo.id);
    return (

                

🎨 Performance Impact of Keys

Keys & Performance: DOM Operations Comparison ✅ Stable Keys (IDs) Scenario: Insert item at position 1 in 1000-item list DOM Operations: ✓ 1 element created (new item) ✓ 999 elements unchanged ✓ Browser just inserts node Performance: ~1ms (fast!) ✓ Component state preserved Input values, scroll positions, etc. kept ⚠️ Index as Key Same scenario: Insert at position 1 DOM Operations: ✗ 999 elements updated (content changed) ✗ 1 element created at end ✗ Browser updates almost everything Performance: ~90ms (slow!) ✗ Component state LOST Inputs reset, forms cleared, bugs! <li> <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> {todo.text} <button onClick={() => onDelete(todo.id)}>Delete</button> </li> ); });

Performance Tips

Tip Why It Helps
Use stable keys (IDs) React can efficiently track items
Extract list items to components Easier to optimize individual items
Use React.memo for items Prevents unnecessary re-renders
Virtualize very long lists Only render visible items
Debounce search/filter Reduces re-renders during typing
Paginate or lazy load Load data as needed

💡 When to Optimize

Don't optimize prematurely! Consider optimization when:

  • List has 500+ items
  • Items are complex components
  • You notice lag when typing or scrolling
  • Profiling shows list is a bottleneck

For most lists under 100 items, basic React is plenty fast!

🏋️ Hands-on Practice

Time to build! These exercises will solidify your list skills.

🏋️ Exercise 1: User Directory

Goal: Build a searchable, sortable user list.

Requirements:

  • Display list of users (name, email, role)
  • Search by name or email
  • Sort by name (A-Z, Z-A)
  • Filter by role (Admin, User, Guest)
  • Show empty state when no results
  • Use proper keys
💡 Starter Data
interface User {
    id: number;
    name: string;
    email: string;
    role: 'Admin' | 'User' | 'Guest';
}

const users: User[] = [
    { id: 1, name: 'Alice Johnson', email: '[email protected]', role: 'Admin' },
    { id: 2, name: 'Bob Smith', email: '[email protected]', role: 'User' },
    { id: 3, name: 'Charlie Brown', email: '[email protected]', role: 'Guest' },
    // Add more...
];

🏋️ Exercise 2: Shopping Cart

Goal: Build a shopping cart with quantity controls.

Requirements:

  • Display cart items with image, name, price, quantity
  • Increase/decrease quantity buttons
  • Remove item button
  • Calculate and display subtotal per item
  • Calculate and display total
  • Empty cart state with "Start Shopping" button
💡 Hint
interface CartItem {
    id: string;
    name: string;
    price: number;
    quantity: number;
    imageUrl: string;
}

const updateQuantity = (id: string, change: number) => {
    setCart(cart.map(item =>
        item.id === id
            ? { ...item, quantity: Math.max(1, item.quantity + change) }
            : item
    ));
};

🏋️ Exercise 3: Dynamic Task Board

Goal: Build a Kanban-style task board.

Requirements:

  • Three columns: "To Do", "In Progress", "Done"
  • Add tasks to "To Do"
  • Move tasks between columns (buttons or drag)
  • Delete tasks
  • Show count of tasks in each column
  • Each column has its own empty state
💡 Hint
type Status = 'todo' | 'inProgress' | 'done';

interface Task {
    id: number;
    text: string;
    status: Status;
}

const moveTask = (id: number, newStatus: Status) => {
    setTasks(tasks.map(task =>
        task.id === id ? { ...task, status: newStatus } : task
    ));
};

const todoTasks = tasks.filter(t => t.status === 'todo');

✨ Best Practices

Follow these guidelines for clean, performant lists.

✅ 1. Always Use Keys

// ✅ Good: Unique, stable key
{items.map(item => <li key={item.id}>{item.name}</li>)}

// ❌ Bad: No key
{items.map(item => <li>{item.name}</li>)}

// ⚠️ Avoid: Index as key (unless list is truly static)
{items.map((item, i) => <li key={i}>{item.name}</li>)}

✅ 2. Extract List Items to Components

// ✅ Good: Clean and reusable
{users.map(user => (
    <UserCard key={user.id} user={user} />
))}

// ❌ Bad: Complex JSX inline
{users.map(user => (
    <div key={user.id}>
        <img src={user.avatar} />
        <h3>{user.name}</h3>
        <p>{user.email}</p>
        <button>View Profile</button>
        {/* 20 more lines... */}
    </div>
))}

✅ 3. Handle Empty States

// ✅ Good: Helpful empty state
{items.length === 0 ? (
    <EmptyState
        title="No items yet"
        description="Add your first item to get started"
    />
) : (
    <ItemList items={items} />
)}

// ❌ Bad: Blank page
{items.map(item => <Item key={item.id} {...item} />)}

✅ 4. Type Your Lists

// ✅ Good: Fully typed
interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

const [todos, setTodos] = useState<Todo[]>([]);

// ❌ Bad: No types
const [todos, setTodos] = useState([]);

✅ 5. Use Derived State

// ✅ Good: Calculate filtered list
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState('all');

const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
});

// ❌ Bad: Store filtered list in state
const [todos, setTodos] = useState<Todo[]>([]);
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);

Lists Checklist

Check Done?
Every list item has a unique key
Keys are stable (not random or index when list changes)
Empty state is handled gracefully
Complex list items extracted to components
List and items are properly typed
Using array methods correctly (map, filter, etc.)
Filtered/sorted lists are derived, not stored

📚 Summary

Congratulations! You've mastered lists and keys in React - a fundamental skill for building dynamic applications.

What You Learned

📋 Rendering Lists

  • Using map() to transform arrays into JSX
  • Rendering arrays of objects
  • Extracting list items to components

🔑 The Key Prop

  • Why keys are critical for React's reconciliation
  • How keys help React track elements
  • Problems with using index as key
  • When index keys are acceptable

🎯 Choosing Keys

  • Best sources for keys (database IDs, UUIDs)
  • Generating keys for new items
  • Keys must be unique among siblings
  • Keys must be stable and consistent

🔧 Array Methods

  • map() for transforming data
  • filter() for selecting items
  • reduce() for aggregating values
  • Chaining methods for complex operations

✏️ CRUD Operations

  • Create: Adding items to lists
  • Read: Displaying lists
  • Update: Modifying items
  • Delete: Removing items

📘 TypeScript

  • Typing arrays and list components
  • Type-safe array methods
  • Generic list components

⚡ Performance

  • How keys affect performance
  • Virtual scrolling for long lists
  • Memoization with React.memo
  • When to optimize

🎯 Key Takeaways

  • Keys are mandatory - React needs them to track list items
  • Use stable IDs - Database IDs or generated UUIDs are best
  • Index as key is risky - Only use for static lists
  • map() is your friend - Transform data to JSX
  • Handle empty states - Don't leave users confused
  • Type everything - TypeScript makes lists safer

🚀 Next Steps

Now that you've mastered lists, you're ready for more advanced rendering patterns:

📖 Coming Up Next

Lesson 3.5: Conditional Rendering Patterns

  • Advanced conditional rendering techniques
  • Ternary operators vs logical && operators
  • Switch statements in JSX
  • Early returns and guard clauses

💪 Practice Projects

Build these to master lists:

  • Contact Manager - CRUD for contacts with search and groups
  • Expense Tracker - List of transactions with categories and totals
  • Recipe Box - Recipes with ingredients lists and instructions
  • Music Playlist - Songs with play counts and favorites
  • Issue Tracker - Bugs/features with status, priority, and assignees

✨ Remember

Lists are everywhere in React applications. The patterns you learned here - proper keys, array methods, CRUD operations - are skills you'll use every single day as a React developer. Master these fundamentals, and you'll be able to build any kind of dynamic, data-driven interface!

  • Issue Tracker - Bugs/features with status, priority, and assignees
  • ✨ Remember

    Lists are everywhere in React applications. The patterns you learned here - proper keys, array methods, CRUD operations - are skills you'll use every single day as a React developer. Master these fundamentals, and you'll be able to build any kind of dynamic, data-driven interface!

    🎉 Congratulation

    🎉 Congratulations!

    You've completed Lesson 3.4 and mastered lists and keys in React! You can now render dynamic lists, choose proper keys, perform CRUD operations, and optimize list performance. This is essential knowledge for building real-world applications. Excellent work! 🚀

    ← Previous Lesson 3.3: Forms in React Home Next → Lesson 3.5: Conditional Rendering