Skip to main content
PProfileMatch

React Lists, Keys and Conditional Rendering

Web DevelopmentLesson 5 of 65 min read

What you'll learn

  • Render arrays of data with map and understand why every item needs a key
  • Choose stable keys and know when using the array index is acceptable
  • Filter lists and show loading, empty and error states
  • Avoid the classic pitfalls of && and ternaries in JSX

On this page

  1. Rendering lists with map
  2. Keys: how React tracks list items
  3. Filtering and derived lists
  4. Conditional rendering: if, ternary and &&
  5. Empty, loading and error states
  6. Common mistakes
  7. How this shows up in interviews and real projects

Most screens are lists of something: todos, messages, products, job postings. Alongside lists comes the question of what to show when there is nothing, or when data is still loading. This lesson covers both.

Rendering lists with map

JSX can embed an array of elements. To turn data into elements, call map on the array and return JSX for each item. There is no special React loop syntax; it is plain JavaScript.

TodoList.tsx
type Todo = { id: number; text: string; done: boolean };

const todos: Todo[] = [
  { id: 1, text: "Write resume", done: true },
  { id: 2, text: "Apply to 3 jobs", done: false },
  { id: 3, text: "Practice React", done: false },
];

export default function TodoList() {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.done ? "[x] " : "[ ] "}
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

Output

[x] Write resume
[ ] Apply to 3 jobs
[ ] Practice React

Keys: how React tracks list items

The key prop is how React matches items between renders. When the list changes, React uses keys to decide which items were added, removed or moved, so it can keep the right DOM nodes and component state attached to the right data. Without keys, React warns in the console and falls back to matching by position, which can show the wrong state after a change.

  • Keys must be unique among siblings, not globally. Two different lists can reuse the same ids.
  • Keys must be stable. Use an id from your data (a database id, a slug, an email). Never generate keys during render with Math.random() or Date.now(); every render would create a new key and remount every item.
  • Index as key is only safe for static lists that are never reordered, filtered or inserted into, and whose items hold no state. Otherwise it causes subtle bugs.
  • Put the key on the outermost element returned by `map`. If you return a fragment, use <Fragment key={id}> imported from React, because the short <> syntax cannot take a key.
  • Keys are not props. The child component does not receive key; pass the id separately if the child needs it.

Here is why the index goes wrong. Suppose each row has an input and you delete the first item. With index keys, the row that used to be second now has key 0, so React reuses the first row's DOM node, including whatever the user had typed in its input. The text appears to jump to the wrong item.

Filtering and derived lists

Do not store a filtered copy of your list in state. Store the original list and the filter choice, and compute the visible items during render. That way there is nothing to keep in sync.

FilterableTodos.tsx
import { useState } from "react";

type Todo = { id: number; text: string; done: boolean };
type Filter = "all" | "active" | "done";

const initialTodos: Todo[] = [
  { id: 1, text: "Write resume", done: true },
  { id: 2, text: "Apply to 3 jobs", done: false },
  { id: 3, text: "Practice React", done: false },
];

export default function FilterableTodos() {
  const [todos] = useState<Todo[]>(initialTodos);
  const [filter, setFilter] = useState<Filter>("all");

  const visible = todos.filter((t) => {
    if (filter === "active") return !t.done;
    if (filter === "done") return t.done;
    return true;
  });

  return (
    <div>
      <button onClick={() => setFilter("all")}>All</button>
      <button onClick={() => setFilter("active")}>Active</button>
      <button onClick={() => setFilter("done")}>Done</button>

      {visible.length === 0 ? (
        <p>Nothing here yet. Try a different filter.</p>
      ) : (
        <ul>
          {visible.map((t) => (
            <li key={t.id}>{t.text}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Output

All:      Write resume / Apply to 3 jobs / Practice React
Active:   Apply to 3 jobs / Practice React
Done:     Write resume
(if a filter matched nothing: "Nothing here yet. Try a different filter.")

Conditional rendering: if, ternary and &&

JSX has no if tag, so you use ordinary JavaScript. There are three patterns, each with a best use. An early return is clearest for whole-component states such as loading or error. A ternary cond ? <A /> : <B /> picks between two outputs inline. The && operator is for "show this or nothing".

NotificationBell.tsx
type NotificationBellProps = {
  count: number;
  loading?: boolean;
};

export default function NotificationBell({ count, loading = false }: NotificationBellProps) {
  // 1. Early return for a whole-component state
  if (loading) return <span>Checking...</span>;

  return (
    <button>
      Bell
      {/* 2. && : badge or nothing. Compare explicitly! */}
      {count > 0 && <span className="badge">{count}</span>}

      {/* 3. Ternary: two different outputs */}
      <small>{count > 0 ? "You have new messages" : "All caught up"}</small>
    </button>
  );
}

Output

count = 3:       Bell 3 You have new messages
count = 0:       Bell All caught up
loading = true:  Checking...

Empty, loading and error states

A list component really has four states: loading, error, empty and populated. Beginners usually build only the last one, and users meet the others in production. An empty state should say what is missing and what to do next ("No saved jobs yet. Browse open roles and save the ones you like."). A loading state prevents a flash of an empty list, and an error state should offer a retry. We wire these to real data in React useEffect: Side Effects and Fetching Data.

Common mistakes

  • Missing keys. Symptom: the console warning "Each child in a list should have a unique key prop".
  • Index keys on a list you reorder or delete from. Symptom: input text or checkbox state sticks to the wrong row after a change.
  • Random keys. key={Math.random()} makes every row remount on every render, so inputs lose focus after each keystroke.
  • `{count && ...}` with a number. Symptom: a stray 0 appears in the UI.
  • Mutating the array while rendering. Calling todos.sort() inside render reorders the original state array. Copy first: [...todos].sort(...).
  • Forgetting to return from `map`. Using braces without return gives undefined for every item, so nothing renders. Use parentheses for an implicit return.

How this shows up in interviews and real projects

"Why do we need keys?" is a classic question, and so is "what is wrong with using the index as a key?". Answer with the reconciliation story above and the stateful-input example. In real work, almost every feature is a list plus filters plus empty and loading states, so getting these four states right is what makes an app feel finished. Strong portfolio projects show them; the coding interview prep guide for self-taught developers covers how to talk about them.

Try it yourself

Build a saved-jobs list from an array of { id, title, company, remote } objects. Add a "Remote only" checkbox that filters the list, and show a friendly empty message when no jobs match. Add a "Remove" button on each row.

Show a hint

Keep the jobs array and a remoteOnly boolean in state. Compute visibleJobs with filter during render, key each row by job.id, and remove with setJobs(jobs.filter(...)).

Key takeaways

  • Render arrays with map, and give every item a stable, unique key from your data.
  • Index keys are only safe for static lists; random keys are always wrong.
  • Store the source list and the filter in state, then derive the visible list during render.
  • Use early returns, ternaries and && deliberately, and compare numbers explicitly to avoid rendering 0.
  • Design for loading, error and empty states, not just the populated list.

Test yourself

A trick from this lesson's ride: A key is a name badge: it lets React recognise the same person after a reshuffle. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

Why does React need a key prop in lists?

Keys let React match list items between renders so it can tell which were added, removed or moved. That keeps the right DOM nodes and component state attached to the right data and avoids unnecessary re-creation of elements.

Can I use the array index as a key in React?

Only when the list is static: never reordered, filtered or inserted into, and with no stateful children such as inputs. Otherwise prefer a stable id from your data, because index keys can attach the wrong state to the wrong row.

Why does React show 0 when I use && for conditional rendering?

Because 0 && something evaluates to 0, and React renders numbers. Use an explicit boolean such as count > 0 && <Badge />, or a ternary like count > 0 ? <Badge /> : null.