Rendering Lists πŸ“‹

1. Introduction πŸ‘‹

Displaying collections of data β€” user lists, product grids, comment threads β€” is one of the most common tasks in web development. React makes this straightforward by leveraging plain JavaScript array methods combined with JSX. This tutorial covers everything from basic list rendering to advanced techniques like virtualization and infinite scrolling.

Information

This tutorial builds on JSX and Conditional Rendering. Reviewing those first will help you follow along more easily.

2. What is List Rendering? πŸ€”

List rendering is the process of transforming an array of data into an array of JSX elements, one for each item, so React can display them on screen.

Code Snippet

const fruits = ["Apple", "Banana", "Cherry"];

function FruitList() {
  return (
    <ul>
      {fruits.map((fruit) => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
}

3. Why Render Lists? πŸ’‘

  • Dynamic Data: Most real applications display data fetched from APIs or databases.
  • Reusable Markup: Avoid repeating near-identical JSX for each item manually.
  • Scalability: The same rendering logic works whether there are 3 items or 3,000.

4. Rendering Arrays πŸ“¦

Since JSX can render arrays of elements directly, transforming a data array into an array of JSX elements is all that's needed to render a list.

Code Snippet

const numbers = [1, 2, 3, 4, 5];

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

5. Using map() πŸ—ΊοΈ

Array.prototype.map() is the standard way to transform data into JSX elements, since it returns a new array without mutating the original.

Code Snippet

const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

function UserList() {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Tip

Always return JSX from the map() callback β€” forgetting the return (or the implicit return with parentheses) is a common source of bugs.

6. Rendering Objects 🧾

Since JSX cannot render plain JavaScript objects directly, you must extract and render their individual properties explicitly.

Code Snippet

const product = { id: 1, name: "Laptop", price: 999 };

function ProductCard({ product }) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
    </div>
  );
}

Danger

Attempting to render a raw object directly (e.g., {product} instead of {product.name}) throws a runtime error: "Objects are not valid as a React child."

7. Rendering Nested Lists πŸͺ†

Data structures with nested arrays require nested map() calls, with a unique key at each level.

Code Snippet

const categories = [
  { id: "c1", name: "Fruits", items: ["Apple", "Banana"] },
  { id: "c2", name: "Vegetables", items: ["Carrot", "Potato"] },
];

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

8. Rendering Components in Lists 🧩

Instead of rendering raw JSX elements inline, list items are often rendered as separate, reusable components β€” improving readability and encapsulation.

Code Snippet

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

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

Important

The key prop must be applied to the outermost element returned in the loop (here, <ProductCard>), not passed down as a regular prop.

9. Understanding Keys πŸ”‘

A key is a special prop that helps React identify which items have changed, been added, or been removed during re-renders, enabling efficient and correct updates.

Code Snippet

{users.map((user) => (
  <li key={user.id}>{user.name}</li>
))}

Note

Keys are used internally by React during reconciliation β€” they are not accessible as a regular prop inside the component itself.

10. Choosing Good Keys βœ…

  • Use a stable, unique identifier from your data, such as a database ID.
  • Keys only need to be unique among siblings, not globally across the entire app.
  • Avoid generating keys with Math.random() or similar β€” they change on every render, defeating their purpose.

Code Snippet

// βœ… Good β€” stable, unique ID from data
{users.map((user) => <li key={user.id}>{user.name}</li>)}

// ❌ Bad β€” regenerates a new key every render
{users.map((user) => <li key={Math.random()}>{user.name}</li>)}

11. Unique Keys πŸ†”

When your data lacks a natural unique identifier, generate one when the data is created β€” for example, using a UUID β€” rather than relying on unstable values at render time.

Code Snippet

import { v4 as uuidv4 } from 'uuid';

function addTodo(text) {
  return { id: uuidv4(), text, completed: false };
}

Tip

Assign the unique ID once, when the item is created β€” not inside the render function, where it would regenerate on every render.

12. Index as a Key ⚠️

Using the array index as a key is acceptable only when the list is static β€” never reordered, filtered, or modified.

Code Snippet

// Acceptable only for static, never-reordered lists
{staticLabels.map((label, index) => (
  <span key={index}>{label}</span>
))}

Danger

Using index as a key in a dynamic list (items added, removed, or reordered) can cause React to mismatch state between items, leading to bugs like form inputs retaining the wrong values.

13. Keyed Fragments 🧩

When a list item needs to render multiple sibling elements without an extra wrapper <div>, use the explicit Fragment form with a key, since the shorthand <> syntax doesn't accept props.

Code Snippet

import { Fragment } from 'react';

function DefinitionList({ terms }) {
  return (
    <dl>
      {terms.map((term) => (
        <Fragment key={term.id}>
          <dt>{term.word}</dt>
          <dd>{term.definition}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

14. Dynamic Lists πŸ”„

Lists backed by state can grow, shrink, and reorder in response to user actions, always producing a new array via immutable updates.

Code Snippet

function TodoList() {
  const [todos, setTodos] = useState([]);

  function addTodo(text) {
    setTodos([...todos, { id: crypto.randomUUID(), text }]);
  }

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

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => removeTodo(todo.id)}>Remove</button>
        </li>
      ))}
    </ul>
  );
}

15. Conditional List Rendering πŸ”€

Combine list rendering with conditional logic to show alternate content, such as an empty state, when there's no data to display.

Code Snippet

function CommentList({ comments }) {
  if (comments.length === 0) {
    return <p>No comments yet. Be the first! ✨</p>;
  }

  return (
    <ul>
      {comments.map((comment) => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

16. Filtering Lists πŸ”

Use Array.prototype.filter() before map() to render only the items matching a specific condition.

Code Snippet

function ActiveUserList({ users }) {
  const activeUsers = users.filter((user) => user.isActive);

  return (
    <ul>
      {activeUsers.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Tip

For search or filter inputs, combine a useState-driven search term with filter() to build a live, responsive filtered list.

17. Sorting Lists πŸ“Š

Use Array.prototype.toSorted() (or spread into a new array before sort()) to order list items without mutating the original array.

Code Snippet

function SortedProductList({ products }) {
  const sortedProducts = [...products].sort((a, b) => a.price - b.price);

  return (
    <ul>
      {sortedProducts.map((product) => (
        <li key={product.id}>{product.name} β€” ${product.price}</li>
      ))}
    </ul>
  );
}

Danger

Calling products.sort() directly mutates the original array in place β€” always sort a copy using the spread operator or toSorted().

18. Grouping Lists πŸ—‚οΈ

Use Array.prototype.reduce() to group flat data into categories before rendering.

Code Snippet

function groupByCategory(products) {
  return products.reduce((groups, product) => {
    const key = product.category;
    if (!groups[key]) groups[key] = [];
    groups[key].push(product);
    return groups;
  }, {});
}

function GroupedProductList({ products }) {
  const grouped = groupByCategory(products);

  return (
    <div>
      {Object.entries(grouped).map(([category, items]) => (
        <div key={category}>
          <h3>{category}</h3>
          <ul>
            {items.map((item) => <li key={item.id}>{item.name}</li>)}
          </ul>
        </div>
      ))}
    </div>
  );
}

19. Flattening Lists πŸ“

Use Array.prototype.flat() or flatMap() to render a single-level list from nested array structures.

Code Snippet

const nestedTags = [["react", "jsx"], ["css", "html"]];
const flatTags = nestedTags.flat();

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

20. Empty Lists πŸ“­

Code Snippet

function TaskList({ tasks }) {
  if (tasks.length === 0) {
    return (
      <div className="empty-state">
        <p>πŸŽ‰ No tasks remaining!</p>
      </div>
    );
  }

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

Best Practice

Always design a clear empty state β€” rendering nothing at all can look like a bug rather than an intentional "no data" state.

21. Loading Lists ⏳

Code Snippet

function ProductList({ products, isLoading }) {
  if (isLoading) {
    return <p>Loading products... ⏳</p>;
  }

  return (
    <ul>
      {products.map((product) => <li key={product.id}>{product.name}</li>)}
    </ul>
  );
}

Tip

Consider using skeleton loaders that mimic the shape of the eventual list content, providing a smoother perceived loading experience than a generic spinner.

22. Paginated Lists πŸ“„

Pagination splits a large dataset into discrete pages, reducing the amount of data rendered and fetched at once.

Code Snippet

function PaginatedList({ items, pageSize = 10 }) {
  const [page, setPage] = useState(1);
  const start = (page - 1) * pageSize;
  const visibleItems = items.slice(start, start + pageSize);
  const totalPages = Math.ceil(items.length / pageSize);

  return (
    <div>
      <ul>
        {visibleItems.map((item) => <li key={item.id}>{item.name}</li>)}
      </ul>
      <button disabled={page === 1} onClick={() => setPage(page - 1)}>Previous</button>
      <span> Page {page} of {totalPages} </span>
      <button disabled={page === totalPages} onClick={() => setPage(page + 1)}>Next</button>
    </div>
  );
}

23. Infinite Scrolling Lists ♾️

Infinite scrolling loads more items automatically as the user approaches the bottom of the list, commonly implemented using the IntersectionObserver API.

Code Snippet

function InfiniteList({ items, loadMore, hasMore }) {
  const observerRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting && hasMore) {
        loadMore();
      }
    });
    if (observerRef.current) observer.observe(observerRef.current);
    return () => observer.disconnect();
  }, [hasMore, loadMore]);

  return (
    <div>
      <ul>
        {items.map((item) => <li key={item.id}>{item.name}</li>)}
      </ul>
      <div ref={observerRef}>{hasMore && "Loading more..."}</div>
    </div>
  );
}

24. Virtualized Lists πŸš€

List virtualization renders only the items currently visible in the viewport, dramatically improving performance for lists with thousands of items.

Code Snippet

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  return (
    <FixedSizeList height={400} itemCount={items.length} itemSize={35} width="100%">
      {Row}
    </FixedSizeList>
  );
}

Best Practice

Reach for virtualization libraries like react-window or @tanstack/react-virtual once a list regularly renders hundreds or thousands of items.

25. Performance Optimization ⚑

  • Always use stable, unique keys to help React skip unnecessary re-renders of unchanged items.
  • Wrap list item components in memo to avoid re-rendering items whose props haven't changed.
  • Use useMemo to avoid recomputing expensive filtering or sorting on every render.
  • Consider virtualization for very large lists instead of rendering every item at once.

Code Snippet

const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.price - b.price),
  [items]
);

26. Best Practices 🌟

  1. Always provide a stable, unique key for each list item.
  2. Avoid using array index as a key for dynamic, reorderable lists.
  3. Filter, sort, and transform data immutably before mapping to JSX.
  4. Handle loading and empty states explicitly for every list.
  5. Extract complex list items into their own reusable components.
  6. Consider pagination or virtualization for large datasets.

27. Common Mistakes 🚫

  • Forgetting the key prop entirely, resulting in React console warnings and reconciliation bugs.
  • Using array index as a key on lists that get reordered or filtered.
  • Mutating the original array with sort(), splice(), or push() instead of creating a copy.
  • Rendering raw objects directly inside JSX instead of their individual properties.
  • Rendering thousands of items without pagination or virtualization, hurting performance.

Danger

Placing the key prop on the wrong element (e.g., an inner child instead of the outermost mapped element) defeats its purpose entirely and can still cause reconciliation bugs.

28. Frequently Asked Questions ❓

Question

Why does React warn me about missing keys?

Answer

Without a key, React can't reliably track which items changed, were added, or were removed between renders, which can cause incorrect UI updates or lost state.

Question

Is it ever okay to use the array index as a key?

Answer

Yes β€” but only for static lists that are never reordered, filtered, or modified. For any dynamic list, use a stable, unique identifier instead.

Question

How many items should trigger virtualization?

Answer

There's no fixed number, but lists regularly rendering hundreds or thousands of DOM nodes are strong candidates for virtualization to maintain smooth performance.

29. Summary πŸ“

Rendering lists in React relies on standard JavaScript array methods like map(), filter(), and sort(), combined with the crucial key prop for efficient, correct updates. From simple static lists to paginated, infinite-scrolling, or virtualized collections, the same core principles of immutability and stable keys apply throughout.

Summary

With list rendering covered, a great next step is exploring Forms and Controlled Components, which frequently work together with dynamic lists β€” for example, editable to-do items or dynamically added form fields.