Skip to main content

Conditional rendering and lists

In this chapter, you learn about rendering based on conditions and displaying array data as lists.

What you'll learn in this chapter

  • Multiple ways to do conditional rendering (if statement, ternary operator, && operator)
  • Displaying lists with map
  • The importance of the key attribute
  • Filtering and handling empty arrays
info

This chapter uses the project you created in Chapter 3. Start the development server with npm run dev and study while writing code.

Conditional rendering

There are multiple ways to do conditional rendering. *The ternary operator and logical operators are explained in Chapter 2.

Conditional rendering with an if statement

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
return <h1>Welcome back!</h1>
}
return <h1>Please log in</h1>
}

The ternary operator

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<h1>
{isLoggedIn ? 'Welcome back!' : 'Please log in'}
</h1>
)
}

The AND operator (&&)

You can use it when you want to display something only when a condition is true.

function Notification({ count }: { count: number }) {
return (
<div>
{/* The && operator: display the right side only when the left side is true
count > 0 is true → display <span>{count} new</span>
count > 0 is false → display nothing */}
{count > 0 && <span>{count} new</span>}
</div>
)
}
warning

If you use 0 with the && operator, 0 may be displayed as-is. When using a number as the condition, explicitly convert it to a boolean.

// Problem: when count is 0, "0" is displayed
{count && <span>{count} items</span>}

// Solution: explicitly convert to a boolean
{count > 0 && <span>{count} items</span>}

Let's try it: display login state

Create a component that holds a state isLoggedIn and displays "Welcome!" if true and "Please log in" if false.

Hint
  1. Manage the login state with useState<boolean>(false)
  2. Switch the display with a ternary operator
  3. Toggle login/logout with a button
Answer and explanation
import { useState } from 'react'
import './App.css'

function LoginStatus() {
const [isLoggedIn, setIsLoggedIn] = useState(false)

const handleToggleLogin = () => {
setIsLoggedIn(!isLoggedIn)
}

return (
<div>
<h1>{isLoggedIn ? 'Welcome!' : 'Please log in'}</h1>
<button onClick={handleToggleLogin}>
{isLoggedIn ? 'Log out' : 'Log in'}
</button>
</div>
)
}

export default function App() {
return <LoginStatus />
}

Using a ternary operator, we display a different message depending on the value of isLoggedIn. Switching the button's label in the same way makes the current state easy to understand.

The OR operator and nullish coalescing

Use these when setting a fallback value.

function Welcome({ name }: { name?: string }) {
return <h1>Hello, {name || 'Guest'}!</h1>
}

// ?? falls back only for null/undefined
function Display({ value }: { value?: number }) {
return <p>Value: {value ?? 'Not set'}</p>
}

Branching on multiple conditions

Early return

function Status({ status }: { status: string }) {
if (status === 'loading') {
return <p>Loading...</p>
}

if (status === 'error') {
return <p>An error occurred</p>
}

return <p>Done</p>
}

Object mapping

type Status = 'pending' | 'approved' | 'rejected'

// Adding as const fixes each property's type to a string literal type
const statusConfig = {
pending: { label: 'Under review', color: 'orange' },
approved: { label: 'Approved', color: 'green' },
rejected: { label: 'Rejected', color: 'red' },
} as const satisfies Record<Status, { label: string; color: string }>

function StatusBadge({ status }: { status: Status }) {
// Use the value of status (like 'pending') as a key to get the config
// satisfies guarantees that Status matches the key set of statusConfig
const config = statusConfig[status]

return (
<span style={{ color: config.color }}>
{config.label}
</span>
)
}
info

The style as const satisfies Record<Status, ...> is a TypeScript pattern for "checking that it satisfies the type while preserving the literal type information." When you add a value to Status, forgetting to add a key to statusConfig becomes a compile error. Without as const, color widens to the string type, so the trick is to combine both.

Displaying lists

Displaying a list with map

You transform array data into elements with the map() method. *The map method is explained in Chapter 2.

function FruitList() {
const fruits = ['Apple', 'Banana', 'Orange']

return (
<ul>
{/* Transform each element of the array into JSX with map()
fruit => (...) is the function run for each element
key={fruit} uniquely identifies each element (needed for React's update optimization) */}
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
)
}

The key attribute

Each element of a list needs a unique key attribute.

Why is a key needed?

React uses the key to identify which elements changed, were added, or were deleted. Without an appropriate key, React can't update efficiently, which can degrade performance or cause unexpected bugs.

// With a key, React can recognize "only the element with id=2 changed"
[
{ id: 1, name: "A" }, // unchanged
{ id: 2, name: "B'" }, // changed
{ id: 3, name: "C" } // unchanged
]
type User = {
id: number
name: string
}

function UserList({ users }: { users: User[] }) {
return (
<ul>
{/* Use a unique identifier (id) for key
The index (0, 1, 2...) can cause problems when adding or deleting elements */}
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
warning

Use a unique ID for key, not the index. If you use the index, unexpected behavior can occur when adding or deleting elements.

// Not recommended: using the index for key
{items.map((item, index) => (
<li key={index}>{item}</li>
))}

// Recommended: using a unique ID for key
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}

Let's try it: display a product list

Display an array of product names and prices with map, and set an appropriate key.

Hint
  1. Define a type with id, name, and price
  2. Transform the array into elements with map
  3. Use id for key (avoid the index)
Answer and explanation
import './App.css'

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

function ProductList() {
const products: Product[] = [
{ id: 1, name: 'Apple', price: 150 },
{ id: 2, name: 'Banana', price: 100 },
{ id: 3, name: 'Orange', price: 200 },
{ id: 4, name: 'Grape', price: 350 },
{ id: 5, name: 'Melon', price: 1500 }
]

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

export default function App() {
return <ProductList />
}

We use the unique ID product.id for key, not the index. This lets React update efficiently when adding or deleting elements.

Filtering and mapping

type Task = {
id: number
title: string
completed: boolean
}

function TaskList({ tasks }: { tasks: Task[] }) {
// Extract only the elements that match the condition with filter()
// !task.completed → keep only incomplete tasks
const incompleteTasks = tasks.filter(task => !task.completed)

return (
<ul>
{/* Display the filtered result with map */}
{incompleteTasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
)
}

Handling empty arrays

function ItemList({ items }: { items: string[] }) {
// Early return: when the array is empty, return a dedicated UI
// This way, the code below can assume "the array is not empty"
if (items.length === 0) {
return <p>There are no items</p>
}

return (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)
}

Let's try it: filtering

Add a "show only 1000 yen and above" checkbox to the product list and implement filtering.

Hint
  1. Manage the filter state with useState<boolean>(false)
  2. Extract the matching products with filter
  3. Display the filter result with map
  4. Also implement a fallback UI for when the filter result is empty
Answer and explanation
import { useState } from 'react'
import './App.css'

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

function FilteredProductList() {
const [filterExpensive, setFilterExpensive] = useState(false)

const products: Product[] = [
{ id: 1, name: 'Apple', price: 150 },
{ id: 2, name: 'Banana', price: 100 },
{ id: 3, name: 'Orange', price: 200 },
{ id: 4, name: 'Grape', price: 350 },
{ id: 5, name: 'Melon', price: 1500 }
]

const handleFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterExpensive(e.target.checked)
}

// Switch the displayed products with a ternary operator
// filterExpensive is true → only 1000 yen and above
// filterExpensive is false → all products
const filteredProducts = filterExpensive
? products.filter(product => product.price >= 1000)
: products

return (
<div>
<label>
<input
type="checkbox"
checked={filterExpensive}
onChange={handleFilterChange}
/>
Show only 1000 yen and above
</label>
{filteredProducts.length === 0 ? (
<p>No matching products</p>
) : (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
{product.name}: {product.price} yen
</li>
))}
</ul>
)}
</div>
)
}

export default function App() {
return <FilteredProductList />
}

We extract only the products that match the condition with the filter method and display the result with map. We also implement a fallback UI for when the filter result is empty.

Summary

  • Conditional rendering: if statement, ternary operator, &&, ||, ??
  • Lists: transform an array into elements with map()
  • The key attribute: specify a unique ID (avoid the index)
  • Filtering: combine with filter()
  • Empty arrays: display an appropriate fallback UI

In the next chapter, you learn how to handle forms.