Topic 7 of 56 · Full Stack Advanced

Topic 7 : React Lists

Lesson TL;DRTopic 7: React Lists 📖 9 min read · 🎯 intermediate · 🧭 Prerequisites: crudoperations, multiprocessinginnodejs Why this matters Up until now, you've been rendering one thing at a time — one heading,...
9 min read·intermediate·react · lists · keys · conditional-rendering

Topic 7: React Lists

📖 9 min read · 🎯 intermediate · 🧭 Prerequisites: crud-operations, multi-processing-in-nodejs

Why this matters

Up until now, you've been rendering one thing at a time — one heading, one button, one card. But real apps don't work that way. Think about any shopping site, a task list, your Instagram feed — all of it is just an array of data displayed on screen. The moment your data becomes a list, you need a reliable way to turn that array into JSX. That's exactly what this lesson covers: mapping arrays into components, giving each item a stable key prop so React can track changes efficiently, and deciding when to show or hide items based on conditions.

What You'll Learn

  • Render arrays of data as JSX lists using .map()
  • Apply the key prop correctly so React can track list-item identity
  • Conditionally include or exclude items during rendering
  • Compose lists of stateful child components (e.g., UserCard)
  • Manage a dynamic list with add and remove operations driven by a form

The Analogy

Think of a postal sorting facility. Every parcel on the conveyor belt has a unique barcode — without it, workers can't tell which package was rerouted, which is new, and which was removed from the belt. React's key prop is that barcode. When items shift around, React reads the keys to decide the minimum set of DOM operations needed, instead of rebuilding the entire conveyor from scratch. Skip the barcode and the facility grinds to a halt with misrouted parcels — skip the key and React renders incorrectly or warns you loudly in the console.

Chapter 1: Rendering Lists

The most direct way to turn a JavaScript array into React elements is .map(). For every element in the source array you return a JSX node, and React renders all of them in order.

Rule: every JSX node returned from .map() must carry a key prop — a string or number that is unique among siblings in that list.

src/components/NumberList.js

import React from 'react';

function NumberList({ numbers }) {
    const listItems = numbers.map((number) => (
        <li key={number.toString()}>{number}</li>
    ));
    return <ul>{listItems}</ul>;
}

export default NumberList;

The key={number.toString()} call converts the numeric value to a string. For simple arrays where values are already unique, this is fine. For real data, prefer a database-issued id (see Chapter 2).

src/App.js — integrating NumberList

import React from 'react';
import './App.css';
import NumberList from './components/NumberList';

function App() {
    const numbers = [1, 2, 3, 4, 5];
    return (
        <div className="App">
            <h1>Number List</h1>
            <NumberList numbers={numbers} />
        </div>
    );
}

export default App;

Chapter 2: Using Keys in Lists

Keys are React's identity system for list items. Without them, React falls back to positional identity — which breaks whenever items are reordered, inserted, or deleted. Keys should be:

  • Stable — same value across renders for the same logical item
  • Unique among siblings — not globally unique, just within the list
  • Not the array index — using the index as a key defeats the purpose when the list can be reordered or filtered

The canonical source of a good key is a database-issued id field.

src/components/TodoList.js

import React from 'react';

function TodoList({ todos }) {
    const listItems = todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
    ));
    return <ul>{listItems}</ul>;
}

export default TodoList;

src/App.js — integrating TodoList

import React from 'react';
import './App.css';
import TodoList from './components/TodoList';

function App() {
    const todos = [
        { id: 1, text: 'Learn React' },
        { id: 2, text: 'Master Redux' },
        { id: 3, text: 'Explore React Router' },
    ];
    return (
        <div className="App">
            <h1>Todo List</h1>
            <TodoList todos={todos} />
        </div>
    );
}

export default App;

Each todo.id is stable and unique — React can now track exactly which item was added, changed, or removed between renders.

Chapter 3: Conditional Rendering in Lists

Sometimes not every item in the source array should appear in the UI. The cleanest approach inside .map() is a ternary that returns null for items that should be hidden — React renders nothing for null.

src/components/ProductList.js

import React from 'react';

function ProductList({ products }) {
    const listItems = products.map((product) =>
        product.available ? <li key={product.id}>{product.name}</li> : null
    );
    return <ul>{listItems}</ul>;
}

export default ProductList;

Items where available is false silently return null and are omitted from the DOM. An alternative is to .filter() before .map(), which can be cleaner when the filter logic is complex:

const listItems = products
    .filter((product) => product.available)
    .map((product) => <li key={product.id}>{product.name}</li>);

src/App.js — integrating ProductList

import React from 'react';
import './App.css';
import ProductList from './components/ProductList';

function App() {
    const products = [
        { id: 1, name: 'Laptop', available: true },
        { id: 2, name: 'Phone', available: false },
        { id: 3, name: 'Tablet', available: true },
    ];
    return (
        <div className="App">
            <h1>Product List</h1>
            <ProductList products={products} />
        </div>
    );
}

export default App;

Only Laptop and Tablet appear — Phone is filtered out by the available check.

Chapter 4: Handling Lists of Components

Lists don't have to render primitive HTML elements. Each item in a list can be a full React component with its own local state, event handlers, and lifecycle. The key prop goes on the component element, not inside the component's JSX.

src/components/UserCard.js

import React, { useState } from 'react';

function UserCard({ user }) {
    const [liked, setLiked] = useState(false);

    return (
        <div className="user-card">
            <h2>{user.name}</h2>
            <p>{user.email}</p>
            <button onClick={() => setLiked(!liked)}>
                {liked ? 'Unlike' : 'Like'}
            </button>
        </div>
    );
}

export default UserCard;

Each UserCard maintains its own liked state independently — liking Alice does not affect Bob's card.

src/components/UserList.js

import React from 'react';
import UserCard from './UserCard';

function UserList({ users }) {
    const userCards = users.map((user) => (
        <UserCard key={user.id} user={user} />
    ));
    return <div className="user-list">{userCards}</div>;
}

export default UserList;

src/App.js — integrating UserList

import React from 'react';
import './App.css';
import UserList from './components/UserList';

function App() {
    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 className="App">
            <h1>User List</h1>
            <UserList users={users} />
        </div>
    );
}

export default App;

Chapter 5: Using Lists with Forms

Static lists are useful, but real applications need lists that grow and shrink at runtime. The pattern: lift the list into useState, pass mutation functions down as props, and let child components call them.

The class built a three-file solution: a form component, a single-item component, and the orchestrating list component.

src/components/TodoForm.js

import React, { useState } from 'react';

function TodoForm({ addTodo }) {
    const [text, setText] = useState('');

    const handleSubmit = (e) => {
        e.preventDefault();
        if (text.trim()) {
            addTodo(text);
            setText('');
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="Add a new todo"
            />
            <button type="submit">Add</button>
        </form>
    );
}

export default TodoForm;

text.trim() guards against submitting whitespace-only entries. After calling addTodo, setText('') resets the field.

src/components/TodoItem.js

import React from 'react';

function TodoItem({ todo, removeTodo }) {
    return (
        <li>
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>Remove</button>
        </li>
    );
}

export default TodoItem;

src/components/TodoListWithForm.js

import React, { useState } from 'react';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';

function TodoListWithForm() {
    const [todos, setTodos] = useState([
        { id: 1, text: 'Learn React' },
        { id: 2, text: 'Master Redux' },
    ]);

    const addTodo = (text) => {
        const newTodo = { id: Date.now(), text };
        setTodos([...todos, newTodo]);
    };

    const removeTodo = (id) => {
        setTodos(todos.filter((todo) => todo.id !== id));
    };

    return (
        <div>
            <h1>Todo List</h1>
            <TodoForm addTodo={addTodo} />
            <ul>
                {todos.map((todo) => (
                    <TodoItem key={todo.id} todo={todo} removeTodo={removeTodo} />
                ))}
            </ul>
        </div>
    );
}

export default TodoListWithForm;

Key decisions in TodoListWithForm:

  • Date.now() generates a unique numeric id for each new item at creation time — sufficient for a client-side list; replace with a server-issued id in production.
  • addTodo uses the spread operator [...todos, newTodo] to produce a new array, preserving React's immutability requirement.
  • removeTodo uses .filter() to return a new array excluding the removed id — never mutate the state array directly.

src/App.js — integrating TodoListWithForm

import React from 'react';
import './App.css';
import TodoListWithForm from './components/TodoListWithForm';

function App() {
    return (
        <div className="App">
            <TodoListWithForm />
        </div>
    );
}

export default App;
flowchart TD
    App --> TodoListWithForm
    TodoListWithForm -- addTodo prop --> TodoForm
    TodoListWithForm -- todos array + removeTodo prop --> TodoItem
    TodoForm -- calls addTodo --> TodoListWithForm
    TodoItem -- calls removeTodo --> TodoListWithForm
    TodoListWithForm -- useState --> State[(todos state)]

🧪 Try It Yourself

Task: Extend TodoListWithForm so that items can be marked as complete. Clicking a todo's text should toggle a completed boolean on that item, and completed items should render with a strikethrough style.

Success criterion: Clicking "Learn React" strikes it through; clicking again restores normal styling. The Remove button still works on both completed and incomplete items.

Starter snippet — add this to TodoListWithForm:

const toggleTodo = (id) => {
    setTodos(
        todos.map((todo) =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
        )
    );
};

Then update TodoItem to accept toggleTodo and apply:

<span
    onClick={() => toggleTodo(todo.id)}
    style={{ textDecoration: todo.completed ? 'line-through' : 'none', cursor: 'pointer' }}
>
    {todo.text}
</span>

🔍 Checkpoint Quiz

Q1. Why should you avoid using the array index as the key prop for list items?

A) React does not support numeric keys
B) Index-based keys break identity tracking when items are reordered, inserted, or removed
C) Keys must always be strings, and indexes are numbers
D) Using an index causes React to skip rendering some items

Q2. Given this component, what does the browser render when products contains three items where only the first and third have available: true?

function ProductList({ products }) {
    return (
        <ul>
            {products.map((p) =>
                p.available ? <li key={p.id}>{p.name}</li> : null
            )}
        </ul>
    );
}

A) All three items, with the second one greyed out
B) A runtime error because null is invalid inside a <ul>
C) Two <li> elements — the first and third products only
D) One <li> element — only the first available product

Q3. In TodoListWithForm, the addTodo function uses [...todos, newTodo] instead of todos.push(newTodo). Why?

A) push is not available in React components
B) React requires immutable state updates — spread creates a new array, triggering a re-render; push mutates the existing array and React won't detect the change
C) The spread operator is faster than push at runtime
D) push would add the item to the beginning of the list instead of the end

Q4. You have a UserList that renders UserCard components, each with their own liked state. If you reorder the users array passed in as a prop, what happens to each card's liked state — and why?

A1. B — When items are reordered or spliced, indexes shift. React sees the same key at a position and assumes it's the same item, leading to stale state and incorrect reconciliation.

A2. C — null is a valid React render value that simply produces no DOM output, so the second item is silently omitted and only two <li> elements appear.

A3. B — React's state updates must be immutable. push mutates the existing array reference, so React's shallow comparison sees no change and skips the re-render. The spread operator returns a new array reference, correctly signaling that state changed.

A4. Each card's liked state follows its key, not its position. Because UserCard is keyed by user.id, React matches each component instance to its stable id after the reorder — so Alice's liked state stays with Alice's card regardless of where in the list she appears. This is the exact reason stable, unique keys matter.

🪞 Recap

  • Use .map() to transform an array of data into an array of JSX elements, then return that array inside a parent element.
  • Every element produced by .map() must carry a key prop that is stable and unique among siblings — prefer a database id over an array index.
  • Return null inside .map() (or .filter() before .map()) to conditionally omit items without breaking the list structure.
  • Lists of components work identically to lists of HTML elements — place the key on the component tag, not inside the component's own JSX.
  • To manage a dynamic list, lift the array into useState, write pure add/remove helpers that return new arrays (never mutate), and pass the helpers as props to child components.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.