Home / Articles / React Conditional and List Rendering Without Footguns

This article is published in English.

React Conditional and List Rendering Without Footguns

Explicit branches, stable list keys, and patterns that keep conditional UI logic readable when components grow beyond toy examples in production React apps.

1963 words

This walkthrough rebuilds an operable path for: React Conditional Rendering and List Rendering — and the True Nature of key. Focus on contracts, checks, and code you can drop into a repo without guessing intent. For Overview, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

Conditional Rendering — To Show or Not to Show

For Conditional Rendering — To Show or Not to Show, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent.

Approach 1 — Branch with if, and return null when you draw nothing

For Approach 1 — Branch with if, and return null when you draw nothing, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent.

type Props = { isInStock: boolean };

function StockBadge({ isInStock }: Props) {
  if (!isInStock) {
    return null; // out of stock — draw nothing
  }
  return <span className="badge">In stock</span>;
}

Approach 2 — One of Two with the Ternary Operator

For Approach 2 — One of Two with the Ternary Operator, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent. For Approach 2 — One of Two with the Ternary Operator, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

function StockBadge({ isInStock }: Props) {
  return (
    <span className="badge">
      {isInStock ? 'In stock' : 'Out of stock'}
    </span>
  );
}

Approach 3 — “Only When” with &&

For Approach 3 — “Only When” with &&, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit. Prefer explicit conditional rendering over clever short-circuits that hide bugs in production.

function Cart({ count }: { count: number }) {
  return (
    <div>
      <h2>Cart</h2>
      {count > 0 && <p>Items in cart: {count}</p>}
    </div>
  );
}

The Most Common && Trap — the Number 0

For The Most Common && Trap — the Number 0, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product. Prefer explicit conditional rendering over clever short-circuits that hide bugs in production.

function Cart({ count }: { count: number }) {
  return (
    <div>
      <h2>Cart</h2>
      {/* 🔴 trap: if count is 0, "0" shows up on screen */}
      {count && <p>Items in cart: {count}</p>}
    </div>
  );
}

function App() {
  return (
    <>
      <Cart count={0} />
      <Cart count={10} />
    </>
  );
}

export default App;
// ✅ count > 0 is true/false, so it's safe
{count > 0 && <p>Items in cart: {count}</p>}

List Rendering — Drawing an Array in a Loop

For List Rendering — Drawing an Array in a Loop, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility. Prefer explicit conditional rendering over clever short-circuits that hide bugs in production. For List Rendering — Drawing an Array in a Loop, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

type Product = {
  id: number;
  name: string;
  price: number;
};

const products: Product[] = [
  { id: 1, name: 'Mechanical Keyboard', price: 89000 },
  { id: 2, name: 'Wireless Mouse', price: 45000 },
  { id: 3, name: 'USB Hub', price: 23000 },
];

function ProductList() {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} — {product.price.toLocaleString()} won
        </li>
      ))}
    </ul>
  );
}

key — the Name Tag React Uses to Tell Items Apart

For key — the Name Tag React Uses to Tell Items Apart, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit. List keys must be stable business ids, not array indexes, when order can change.

Warning: Each child in a list should have a unique "key" prop.

Keys Should Be “Stable and Unique”

For Keys Should Be “Stable and Unique”, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product. List keys must be stable business ids, not array indexes, when order can change.

<li key={product.id}>   // ✅ each product's unique id — stable

Trap — Don’t Use the Array Index as a Key

For Trap — Don’t Use the Array Index as a Key, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility. List keys must be stable business ids, not array indexes, when order can change. For Trap — Don’t Use the Array Index as a Key, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

// 🔴 common but dangerous pattern
{products.map((product, index) => (
  <li key={index}>{product.name}</li>
))}

Putting It Together — Conditional + List

For Putting It Together — Conditional + List, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent.

type Product = {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
};

function ProductList({ products }: { products: Product[] }) {
  // when the list is empty — conditional rendering
  if (products.length === 0) {
    return <p>No products to display.</p>;
  }

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} — {product.price.toLocaleString()} won
          {/* badge only when out of stock — && (safe since the left side is boolean) */}
          {!product.inStock && <span className="badge"> (Out of stock)</span>}
        </li>
      ))}
    </ul>
  );
}

// dummy data — swap in an empty array [] to see the "No products" message
const products: Product[] = [
  { id: 1, name: 'Mechanical Keyboard', price: 89000, inStock: true },
  { id: 2, name: 'Wireless Mouse', price: 42000, inStock: false },
  { id: 3, name: 'USB-C Hub', price: 35000, inStock: true },
];

function App() {
  return <ProductList products={products} />;
}

Wrapping Up

For Wrapping Up, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries and dead-letter handling are part of the product. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent.

References

For References, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility. Colocate types with components and keep props narrow. Wide prop bags become the debt TypeScript was meant to prevent. For References, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

Operational checklist

For Operational checklist, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state.

Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Prefer explicit conditional rendering over clever short-circuits that hide bugs in production.

Prefer boring reliability over clever one-off demos.

Record timings and cost next to functional results. Visibility early prevents surprise bills in shared environments.

Prefer explicit conditional rendering over clever short-circuits that hide bugs in production.

Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation.