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
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>
)
}
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
- Manage the login state with
useState<boolean>(false) - Switch the display with a ternary operator
- 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>
)
}
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>
)
}
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
- Define a type with
id,name, andprice - Transform the array into elements with
map - Use
idforkey(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
- Manage the filter state with
useState<boolean>(false) - Extract the matching products with
filter - Display the filter result with
map - 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:
ifstatement, ternary operator,&&,||,?? - Lists: transform an array into elements with
map() - The
keyattribute: 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.