Learning path · Level 9 · Tutorial 3

Render Conditional Content and Keyed Lists

Render alternative interface states and collections from typed data, choosing keys that keep item identity stable as the data changes.

  • Level: Intermediate
  • Duration: 14 min
  • 7 steps
Start tutorial Back to the path
Render Conditional Content and Keyed Lists

What you will go through

  1. Choose Between JSX Branches Use an early return for an absent featured product and a ternary expression for its stock label. 2 min
  2. Hide Optional Content Without Showing Zero Render optional note and offer content without accidentally displaying numeric zero. 2 min
  3. Render Typed Records as List Items Map typed product records to semantic list rows with typed props and a supplied sibling key. 2 min
  4. Give Empty Collections a Meaningful Alternative Render an explanatory empty state for an available product collection with no records. 2 min
  5. Keep List Identity Attached to the Record Use stable record identifiers to keep React list identity attached to changing Supply Shelf records. 2 min
  6. Place Keys at the Mapped Boundary Place keys on the sibling elements returned by a map callback, while passing record identifiers separately as ordinary component props. 2 min
  7. Apply and Check the Complete Rendering Pattern Repair and verify a complete typed Supply Shelf interface that covers absent, optional, empty, and reordered content. 3 min

What you will learn

  • Select appropriate JSX branches for available, absent, and empty content.
  • Render a typed collection with a meaningful empty-state alternative.
  • Choose stable sibling keys and place them on the outermost mapped elements.
  • Correct conditional expressions that accidentally display numeric zero.

Before you start

  • Compose Interfaces with Typed Props
  • Transform Arrays with Callback Functions
  • Narrow Unions and Nullable Values

Step 1 of 7

Choose Between JSX Branches

Use an early return for an absent featured product and a ternary expression for its stock label.

Choose the scope of each branch

Guard the whole component

A featured product can be either a Product or null. Use an early return when product === null: it selects the complete fallback before any JSX reads product fields.

Once that guard returns, TypeScript knows product is available in the remaining JSX. Use a ternary expression only for the short stock label, because the heading and product name stay the same.

FeaturedProduct branches

In src/App.tsx, this component handles both the absent and available cases.

tsx
type Product = {
  id: string;
  name: string;
  inStock: boolean;
  note: string | null;
  offerCount: number;
};

type FeaturedProductProps = {
  product: Product | null;
};

const featuredProduct: Product | null = {
  id: "p-101",
  name: "Desk Lamp",
  inStock: true,
  note: null,
  offerCount: 2,
};

function FeaturedProduct({ product }: FeaturedProductProps) {
  if (product === null) {
    return <p>No featured product.</p>;
  }

  return (
    <section>
      <h2>Featured product</h2>
      <p>{product.name}</p>
      <p>{product.inStock ? "In stock" : "Out of stock"}</p>
    </section>
  );
}

export default function App() {
  return (
    <main>
      <h1>Supply Shelf</h1>
      <FeaturedProduct product={featuredProduct} />
    </main>
  );
}

See data become visible output

Compare the rendered branches

The early return changes the entire component output for an absent record. For an available record, the ternary expression changes only the stock words inside shared JSX.

This division keeps each condition focused: one branch for availability, one branch for a small display difference.

Whole fallback versus inline label

A comparison diagram: null product data renders only the No featured product fallback, while available products share the same section and differ only between In stock and Out of stock labels.

An early return chooses a whole-component fallback; a ternary expression chooses one short value within shared JSX.

Practice: complete both decisions

Complete the component branches

Complete the two JSX branch expressions in this component.

tsx
function FeaturedProduct({ product }: FeaturedProductProps) {
  if () {
    return <p>No featured product.</p>;
  }

  return (
    <section>
      <h2>Featured product</h2>
      <p>{product.name}</p>
      <p></p>
    </section>
  );
}

Practice: predict each rendered result

Connect props to output

Match each supplied product prop case to its visible output.

Tap an item and then its matching pair.

Step 2 of 7

Hide Optional Content Without Showing Zero

Render optional note and offer content without accidentally displaying numeric zero.

Render nothing on purpose

Optional note, optional output

A component can intentionally render nothing. ProductNote receives a nullable note; when the value is absent, its early return is null. React adds no element and no placeholder text for that component.

Once the note === null guard has passed, TypeScript treats note as a string, so the remaining JSX can display it.

ProductNote returns no content for an absent note

Use this helper in src/App.tsx when a note should occupy no interface space if it is absent.

tsx
type ProductNoteProps = {
  note: string | null;
};

function ProductNote({ note }: ProductNoteProps) {
  if (note === null) {
    return null;
  }

  return <p>{note}</p>;
}

Prevent a numeric zero from leaking

Make the condition Boolean

Logical AND can conditionally include a short JSX fragment. With a Boolean condition, true && <p>...</p> produces the paragraph, while false && <p>...</p> produces false, which React does not display.

Do not use a number itself as the left side. When offerCount is 0, 0 && <p>...</p> evaluates to the numeric value 0; React renders that value as visible text. Compare the count explicitly so the left side is always a Boolean.

Compare the visible results

Three Supply Shelf card outputs: an absent note leaves no content, a numeric AND condition displays zero, and a Boolean comparison hides the offer message when the count is zero.

An absent note and a hidden offer message leave no visible row. A numeric zero is different: React can render it as text.

Repair the offer condition

Both fragments test the same offer message. Only the second condition is guaranteed to be Boolean.

tsx
{/* Problem: when offerCount is 0, this expression evaluates to 0. */}
{product.offerCount && <p>Offers: {product.offerCount}</p>}

{/* A Boolean false produces no visible content. */}
{product.offerCount > 0 && <p>Offers: {product.offerCount}</p>}

Check the expression result

Check the zero case

When product.offerCount is 0, can this JSX expression display a visible 0?

{product.offerCount && <p>Offers: {product.offerCount}</p>}

Apply the conditions

Apply both conditional patterns

Complete the two missing expressions: one for an absent optional note and one for an offer message that should appear only for a positive count.

tsx
type ProductNoteProps = {
  note: string | null;
};

type FeaturedProductProps = {
  product: Product | null;
};

function ProductNote({ note }: ProductNoteProps) {
  if (note === null) {
    return ;
  }

  return <p>{note}</p>;
}

function FeaturedProduct({ product }: FeaturedProductProps) {
  if (product === null) {
    return <p>No featured product.</p>;
  }

  return (
    <section>
      <h2>Featured product</h2>
      <p>{product.name}</p>
      {product.offerCount  && <p>Offers: {product.offerCount}</p>}
    </section>
  );
}

Step 3 of 7

Render Typed Records as List Items

Map typed product records to semantic list rows with typed props and a supplied sibling key.

Map records through a list component

From records to rows

ProductList receives a typed Product[] and turns it into sibling ProductRow elements with products.map(...). Each callback result becomes one row because ProductRow returns one <li>.

The array order becomes the visible row order. ProductList supplies the surrounding <ul>, while each row receives only the fields in its prop contract: productId, name, and inStock.

Add key={product.id} to every mapped row from the start. Here, the key is React's sibling identifier; later, you will examine why the record identifier is the right choice.

Trace one record into one list item

A diagram connects three product records, in array order, to ProductRow elements and then to matching rows inside one unordered list.

The map callback preserves the array order: each Product record produces one ProductRow, which produces one list item.

Read the component boundary

A typed Supply Shelf list

Place this complete example in src/App.tsx. The three records are mapped in their array order, so the shelf shows three rows in that same order.

tsx
type Product = { id: string; name: string; inStock: boolean; note: string | null; offerCount: number; };

const products: Product[] = [
  { id: "p-101", name: "Desk Lamp", inStock: true, note: null, offerCount: 2 },
  { id: "p-102", name: "Notebook", inStock: false, note: "Back next week.", offerCount: 0 },
  { id: "p-103", name: "Canvas Bag", inStock: true, note: null, offerCount: 1 }
];

type ProductRowProps = {
  productId: string;
  name: string;
  inStock: boolean;
};

type ProductListProps = { products: Product[]; };

function ProductRow({ productId, name, inStock }: ProductRowProps) {
  return <li>{name} — Item: {productId} — {inStock ? "In stock" : "Out of stock"}</li>;
}

function ProductList({ products }: ProductListProps) {
  return (
    <ul>
      {products.map((product) => (
        <ProductRow key={product.id} productId={product.id} name={product.name} inStock={product.inStock} />
      ))}
    </ul>
  );
}

export default function App() {
  return <main><h1>Supply Shelf</h1><ProductList products={products} /></main>;
}
Expected output
Supply Shelf
Desk Lamp — Item: p-101 — In stock
Notebook — Item: p-102 — Out of stock
Canvas Bag — Item: p-103 — In stock

When the complete file runs, the browser renders these visible text lines; HTML list markers may also appear depending on browser styling.

Practice the mapping pattern

Complete the mapped row

Complete the mapping callback and the remaining row prop expressions. Keep the supplied key unchanged.

tsx
function ProductList({ products }: ProductListProps) {
  return (
    <ul>
      {products.((product) => (
        <ProductRow
          key={product.id}
          productId={product.id}
          name={product.}
          inStock={product.}
        />
      ))}
    </ul>
  );
}

Predict the rendered shelf

Apply the list pattern

Write the products.map(...) JSX expression for ProductList, including the supplied key and all ProductRow props. Then predict how many rows render from the fixture array and give their record identifier order.

Write at least 80 characters (0/80).

Step 4 of 7

Give Empty Collections a Meaningful Alternative

Render an explanatory empty state for an available product collection with no records.

Recognize an empty collection

Empty is not absent

featuredProduct can be null, which means that one record is absent. In contrast, products is always a Product[]. When it contains [], the collection is available but has no records.

Give that valid empty collection a visible empty state. Do not render an unexplained empty <ul>.

Two valid collection states

A comparison of a populated product array rendered as a list and an empty product array rendered as an explanatory message without a list.

A populated collection produces a list. An empty collection produces an empty state that explains why no rows appear.

Replace the unexplained list

Guard the list branch

Check the array length before creating the list container. The early return replaces the entire list branch: for an empty array, there is no <ul> at all.

Do not use if (products). Even [] is truthy in JavaScript, so that check cannot distinguish an empty collection from a populated one.

ProductList with an empty state

A ProductList owns the collection-level branch, while each ProductRow still returns exactly one list item.

tsx
type Product = {
  id: string;
  name: string;
  inStock: boolean;
  note: string | null;
  offerCount: number;
};

type ProductListProps = {
  products: Product[];
};

type ProductRowProps = {
  productId: string;
  name: string;
  inStock: boolean;
};

function ProductRow({ productId, name, inStock }: ProductRowProps) {
  return (
    <li>
      Item: {productId} — {name} — {inStock ? "In stock" : "Out of stock"}
    </li>
  );
}

function ProductList({ products }: ProductListProps) {
  if (products.length === 0) {
    return <p>No products available.</p>;
  }

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

Apply the empty-state rule

Choose the empty-array guard

ProductList must handle both fixtures: an empty products array should show an explanatory paragraph and no <ul>; a populated array should render its <ul> and rows. Which branch belongs immediately before the list JSX?

Step 5 of 7

Keep List Identity Attached to the Record

Use stable record identifiers to keep React list identity attached to changing Supply Shelf records.

Follow the record, not its position

Identity survives a changed order

When a mapped list changes, React uses keys to match sibling elements with their corresponding records across renders. A key should remain attached to the record—not to where that record happens to appear today.

Start with p-101 → p-102 → p-103. If p-102 is removed and the order becomes p-103 → p-101, the remaining records still have the same record identifiers. Their positions changed, but their identities did not.

Record identity is not array position

A before-and-after list diagram shows p-101, p-102, and p-103 initially. After removal and reordering, p-103 and p-101 retain their identifiers while occupying different positions.

The record identifier stays with its token; a numbered position does not.

Compare key strategies

Choose a stable sibling key

A stable key is unique among the current siblings and continues to mean the same record when the collection changes. It does not need to be globally unique across the whole application.

For Supply Shelf, product.id is a stable record identifier. An array index identifies a position, so it can point to a different record after removal or reordering. A random value is worse: it changes on every render.

Stable keys help React perform correct identity matching. They do not promise that a component will not render.

Key strategies

Compare each candidate against a list that changes from p-101 → p-102 → p-103 to p-103 → p-101.

A key must remain meaningful for the same sibling record.
Candidate keyAttached toAfter the list changesSuitable?
`product.id`The recordStill identifies that recordYes
Array indexCurrent positionMay identify another recordNo
`Math.random()`One render onlyChanges on every renderNo

Practice recognizing stability

Example

Check the key rule

Match each key choice to its consequence in a changing sibling list.

Match key sources

Match each key source with what it follows.

Tap an item and then its matching pair.

Apply stable record identity

Apply the identity rule

The initial order is p-101 → p-102 → p-103. The changed order is p-103 → p-101. Which plan correctly matches the surviving records and chooses an appropriate sibling key?

Step 6 of 7

Place Keys at the Mapped Boundary

Place keys on the sibling elements returned by a map callback, while passing record identifiers separately as ordinary component props.

Identify the mapped boundary

Key belongs to the returned sibling

A map callback creates a set of siblings. Put the key on the outermost element that the callback returns, because that is the boundary where React compares those siblings.

If the callback returns an <li>, the <li> receives the key. If it returns a <ProductRow />, the ProductRow receives the key. A stable key uses the record identifier, such as product.id.

key is special: React consumes it while matching siblings. It is not delivered to ProductRow as a normal prop.

React key versus a readable prop

Diagram showing key on ProductRow siblings created by a map callback and productId passed separately into a ProductRow child.

React matches siblings with a key at the parent boundary; ProductRow reads its record identifier through productId.

Separate React identity from component props

Pass what the child needs

In this Supply Shelf mapping, the callback returns ProductRow, so key={product.id} belongs on ProductRow. The child receives productId separately because it needs to display Item: p-101-style identifiers.

The stock label remains ordinary rendering logic inside the child.

Keyed ProductRow mapping

Use this pattern in src/App.tsx. The parent supplies both React's sibling identifier and the child-readable record identifier.

tsx
type ProductRowProps = {
  productId: string;
  name: string;
  inStock: boolean;
};

function ProductRow({ productId, name, inStock }: ProductRowProps) {
  return (
    <li>
      Item: {productId} — {name} — {inStock ? "In stock" : "Out of stock"}
    </li>
  );
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map((product) => (
        <ProductRow
          key={product.id}
          productId={product.id}
          name={product.name}
          inStock={product.inStock}
        />
      ))}
    </ul>
  );
}

Warning

Avoid the inner-key mistake

Do not place a key only on the <li> inside ProductRow. The parent mapping returns ProductRow siblings, not <li> siblings, so an inner key does not identify the elements React is matching.

Also, key cannot replace productId: a component cannot read key from its props.

Apply the key and prop rules

Code to repair

This version has an inner key and omits the child-readable identifier from the parent mapping. Repair it locally before answering.

tsx
type ProductRowProps = {
  productId: string;
  name: string;
  inStock: boolean;
};

function ProductRow({ productId, name, inStock }: ProductRowProps) {
  return (
    <li key={productId}>
      Item: {productId} — {name} — {inStock ? "In stock" : "Out of stock"}
    </li>
  );
}

function ProductList({ products }: { products: Product[] }) {
  return (
    <ul>
      {products.map((product) => (
        <ProductRow
          name={product.name}
          inStock={product.inStock}
        />
      ))}
    </ul>
  );
}

Repair the mapped boundary

Repair the code. Move the stable key to the mapped ProductRow, remove the key from the child <li>, and pass the record identifier through productId so the child can display it.

Paste the corrected ProductRow return and the corrected mapped ProductRow element.

Write at least 120 characters (0/120).

Step 7 of 7

Apply and Check the Complete Rendering Pattern

Repair and verify a complete typed Supply Shelf interface that covers absent, optional, empty, and reordered content.

Plan the local verification

Integrate the rendering decisions

Build the complete Supply Shelf pattern in your local src/App.tsx. It combines:

  • an early return for an absent featured product;
  • a ProductNote that returns null for an absent note;
  • an explicit positive-count check before showing offers;
  • a semantic list with an empty state;
  • record-based keys on the mapped ProductRow siblings.

Copy the three code sections in order. Then inspect each static variation with your local development server. Change one fixture at a time and restore it before the next check.

Stable identity through a changed order

A before-and-after list diagram shows p-101, p-102, and p-103 initially, then p-103 followed by p-101. Arrows keep each surviving identifier attached to its record rather than its array position.

A stable key follows the record identifier, not the current array position. Source edits and hot reload do not prove preserved component identity.

Static checks to make

Use these local checks after assembling the file. They verify visible rendering; the changed-order check lets you reason about stable keys from the source and diagram.

Edit static values, inspect the browser, then restore the initial fixtures.
ChangeVisible evidence
Initial fixturesDesk Lamp is featured with In stock and Offers: 2; all three list rows appear.
Set featuredProduct to nullThe featured area shows No featured product.
Set featuredProduct to products[1]Notebook shows Out of stock and Back next week.; no offers line appears.
Set products to [] and featuredProduct to nullThe list area shows No products available. instead of an empty list.
Keep only p-103 then p-101Rows follow that order; each surviving row keeps its own record identifier as its stable key.

Assemble the complete local file

Add the typed fixtures and App

Part 1 of 3 — place this first in src/App.tsx. This is the initial section of the file, not a runnable file by itself; the helper components follow in Parts 2 and 3.

tsx
type Product = {
  id: string;
  name: string;
  inStock: boolean;
  note: string | null;
  offerCount: number;
};

const products: Product[] = [
  { id: "p-101", name: "Desk Lamp", inStock: true, note: null, offerCount: 2 },
  { id: "p-102", name: "Notebook", inStock: false, note: "Back next week.", offerCount: 0 },
  { id: "p-103", name: "Canvas Bag", inStock: true, note: null, offerCount: 1 }
];

const featuredProduct: Product | null = products[0];

export default function App() {
  return (
    <main>
      <h1>Supply Shelf</h1>
      <FeaturedProduct product={featuredProduct} />
      <ProductList products={products} />
    </main>
  );
}

Add the featured and optional-content components

Part 2 of 3 — append these typed helper components below Part 1 in the same file.

tsx
type FeaturedProductProps = {
  product: Product | null;
};

type ProductNoteProps = {
  note: string | null;
};

type ProductListProps = {
  products: Product[];
};

type ProductRowProps = {
  productId: string;
  name: string;
  inStock: boolean;
};

function FeaturedProduct({ product }: FeaturedProductProps) {
  if (product === null) {
    return (
      <section>
        <h2>Featured product</h2>
        <p>No featured product.</p>
      </section>
    );
  }

  return (
    <section>
      <h2>Featured product</h2>
      <p>{product.name}</p>
      <p>{product.inStock ? "In stock" : "Out of stock"}</p>
      <ProductNote note={product.note} />
      {product.offerCount > 0 && <p>Offers: {product.offerCount}</p>}
    </section>
  );
}

function ProductNote({ note }: ProductNoteProps) {
  if (note === null) {
    return null;
  }

  return <p>{note}</p>;
}

Complete the keyed list and run the file

Part 3 of 3 — append this below Part 2. It completes the executable src/App.tsx file. Run your existing local development server and compare the initial browser output with the result below.

tsx
function ProductList({ products }: ProductListProps) {
  if (products.length === 0) {
    return <p>No products available.</p>;
  }

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

function ProductRow({ productId, name, inStock }: ProductRowProps) {
  return (
    <li>
      {name} — Item: {productId} — {inStock ? "In stock" : "Out of stock"}
    </li>
  );
}
Expected output
Supply Shelf
Featured product
Desk Lamp
In stock
Offers: 2
Desk Lamp — Item: p-101 — In stock
Notebook — Item: p-102 — Out of stock
Canvas Bag — Item: p-103 — In stock

Rendered browser text for the initial fixtures. Desk Lamp has no visible note because ProductNote returns null for its null note.

Apply, inspect, and explain

Repair the integrated pattern

Complete the integrated rendering repairs. The surrounding types and components are available in your local file.

tsx
function ProductNote({ note }: ProductNoteProps) {
  if (note === null) {
    return ;
  }

  return <p>{note}</p>;
}

function FeaturedProduct({ product }: FeaturedProductProps) {
  if (product  null) {
    return (
      <section>
        <h2>Featured product</h2>
        <p>No featured product.</p>
      </section>
    );
  }

  return (
    <section>
      <h2>Featured product</h2>
      <p>{product.inStock ? "In stock" : "Out of stock"}</p>
      <ProductNote note={product.note} />
      {product.offerCount  0 && <p>Offers: {product.offerCount}</p>}
    </section>
  );
}

function ProductList({ products }: ProductListProps) {
  if (products.length  0) {
    return <p>No products available.</p>;
  }

  return (
    <ul>
      {products.map((product) => (
        <ProductRow
          key={}
          productId={product.}
          name={product.name}
          inStock={product.inStock}
        />
      ))}
    </ul>
  );
}

Verify the complete rendering pattern

In your local app, apply the static checks and write four short lines:

  1. The exact visible empty-state message.
  2. When Notebook is the featured product: its stock label, its note, and whether an offers line appears.
  3. What you observe for Desk Lamp's absent note.
  4. After changing the list to Canvas Bag followed by Desk Lamp: the two key values and why they remain associated with records rather than their new positions.

Confirm that you applied the repaired conditions before reporting the results.

Write at least 210 characters (0/210).

Keep the pattern ready

Summary

Decision summary

Use this compact decision guide whenever typed data can be absent, empty, optional, or reordered.

  • Use an early return when an absent record needs a whole-component fallback.
  • Use a ternary expression for short alternatives inside shared JSX, such as a stock label.
  • Return null when optional content should take no space.
  • For a numeric count, use an explicit positive comparison before rendering JSX; do not let numeric zero become visible text.
  • Use products.length === 0 for an explanatory empty state instead of an unexplained empty list.
  • Map records to semantic list items and put a stable key on the outermost mapped sibling.
  • Use the record identifier for the key and pass productId separately when ProductRow needs to read it.

Tutorial completed

Congratulations! You completed: Render Conditional Content and Keyed Lists

Complete. You can now select clear JSX alternatives, render typed collections with meaningful empty states, and preserve list identity with record-based keys.

20 XP

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate