Списки та умовний рендеринг
React списки та умовний рендеринг — меню-дошка в кафе де деякі страви мають мітку sold-out
У попередньому уроці ми працювали зі state та навіть трохи рендерили списки. Тепер розберемо це детально: як правильно виводити масиви даних та показувати різний UI залежно від умов.
Рендеринг списків з .map()
У React для відображення масивів використовуємо .map() -- метод, який ти вже знаєш з Block 4:
function FruitList() {
const fruits = ['Яблуко', 'Банан', 'Апельсин', 'Манго']
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
)
}
Зі складнішими даними
interface Product {
id: number
name: string
price: number
inStock: boolean
}
const products: Product[] = [
{ id: 1, name: 'Ноутбук', price: 25000, inStock: true },
{ id: 2, name: 'Навушники', price: 2500, inStock: false },
{ id: 3, name: 'Клавіатура', price: 1800, inStock: true },
]
function ProductList() {
return (
<div className="products">
{products.map((product) => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>{product.price} грн</p>
<span>{product.inStock ? 'В наявності' : 'Немає'}</span>
</div>
))}
</div>
)
}
Key -- навіщо та як
Кожен елемент списку обов'язково повинен мати prop key -- унікальний ідентифікатор:
// ДОБРЕ -- унікальний id
{users.map(user => (
<UserCard key={user.id} name={user.name} />
))}
Навіщо key?
Коли масив змінюється (додається, видаляється, сортується елемент), React використовує key для визначення, який саме елемент змінився. Без key React буде перемальовувати весь список, що повільно і може спричинити баги.
Правила для key
// ДОБРЕ: id з даних
{products.map(p => <Card key={p.id} {...p} />)}
// ДОБРЕ: унікальний рядок
{tags.map(tag => <Tag key={tag} text={tag} />)}
// ПОГАНО: індекс масиву (тільки якщо список статичний!)
{items.map((item, index) => <li key={index}>{item}</li>)}
Не використовуй індекс масиву як key, якщо список може змінюватись (додавання, видалення, сортування). При зміні порядку React "загубить" стан елементів. Індекс підходить тільки для повністю статичних списків.
Що підходить як key?
idз бази даних або API -- найкращий варіант- Унікальна комбінація полів:
key={`${user.name}-${user.email}`} crypto.randomUUID()абоDate.now()-- тільки при створенні елемента, не в.map()
// ПОГАНО: генерує новий key на кожен рендер!
{items.map(item => <li key={Math.random()}>{item}</li>)}
// ДОБРЕ: id створюється один раз при додаванні
function addItem(text: string) {
setItems(prev => [...prev, { id: crypto.randomUUID(), text }])
}
Умовний рендеринг
React дозволяє показувати різний UI залежно від умов. Є кілька способів.
Спосіб 1: Тернарний оператор
Для вибору між двома варіантами:
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn ? (
<h1>Ласкаво просимо!</h1>
) : (
<h1>Будь ласка, увійдіть</h1>
)}
</div>
)
}
Спосіб 2: Логічне && (AND)
Для показу або приховування елемента:
function Notifications({ count }: { count: number }) {
return (
<div>
<h1>Сповіщення</h1>
{count > 0 && <span className="badge">{count}</span>}
</div>
)
}
Обережно з && та числом 0! Вираз {0 && <Component />} відрендерить 0 на екрані (бо 0 -- falsy, але React його рендерить). Використовуй {count > 0 && ...} замість {count && ...}.
Спосіб 3: Early return
Для компонентів, які нічого не показують за певної умови:
interface UserProfileProps {
user: { name: string; email: string } | null
}
function UserProfile({ user }: UserProfileProps) {
if (!user) {
return <p>Користувач не знайдений</p>
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
Спосіб 4: Змінна з JSX
Для складної логіки:
type Status = 'loading' | 'success' | 'error'
function StatusMessage({ status }: { status: Status }) {
let content: React.ReactNode
if (status === 'loading') {
content = <p>Завантаження...</p>
} else if (status === 'success') {
content = <p className="success">Успіх!</p>
} else {
content = <p className="error">Помилка!</p>
}
return <div className="status">{content}</div>
}
Умовні CSS-класи
interface ButtonProps {
isActive: boolean
text: string
}
function Button({ isActive, text }: ButtonProps) {
return (
<button className={`btn ${isActive ? 'btn-active' : ''}`}>
{text}
</button>
)
}
Для кількох умовних класів:
interface CardProps {
isSelected: boolean
isDisabled: boolean
size: 'small' | 'large'
}
function Card({ isSelected, isDisabled, size }: CardProps) {
const classes = [
'card',
`card-${size}`,
isSelected ? 'card-selected' : '',
isDisabled ? 'card-disabled' : '',
].filter(Boolean).join(' ')
return <div className={classes}>...</div>
}
У реальних проектах для умовних класів часто використовують бібліотеку clsx: className={clsx('card', { 'card-active': isActive })}. Вона компактніша і зручніша.
Практика: фільтрований список
Поєднаємо списки, умовний рендеринг та state:
import { useState } from 'react'
interface Task {
id: number
text: string
done: boolean
priority: 'low' | 'medium' | 'high'
}
const initialTasks: Task[] = [
{ id: 1, text: 'Вивчити React', done: false, priority: 'high' },
{ id: 2, text: 'Зробити домашку', done: true, priority: 'medium' },
{ id: 3, text: 'Почитати документацію', done: false, priority: 'low' },
{ id: 4, text: 'Написати компонент', done: false, priority: 'high' },
]
type Filter = 'all' | 'active' | 'done'
function TaskList() {
const [tasks, setTasks] = useState<Task[]>(initialTasks)
const [filter, setFilter] = useState<Filter>('all')
const filteredTasks = tasks.filter(task => {
if (filter === 'active') return !task.done
if (filter === 'done') return task.done
return true
})
function toggleTask(id: number) {
setTasks(prev =>
prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
)
}
return (
<div>
<h1>Завдання ({filteredTasks.length})</h1>
<div className="filters">
{(['all', 'active', 'done'] as Filter[]).map(f => (
<button
key={f}
className={filter === f ? 'active' : ''}
onClick={() => setFilter(f)}
>
{f === 'all' ? 'Всі' : f === 'active' ? 'Активні' : 'Виконані'}
</button>
))}
</div>
{filteredTasks.length === 0 ? (
<p>Немає завдань</p>
) : (
<ul>
{filteredTasks.map(task => (
<li
key={task.id}
className={task.done ? 'done' : ''}
onClick={() => toggleTask(task.id)}
>
{task.done ? '✓' : '○'} {task.text}
{task.priority === 'high' && (
<span className="priority-high"> !</span>
)}
</li>
))}
</ul>
)}
</div>
)
}
export default TaskList
Підсумок
.map()-- основний спосіб рендерити списки в React- key -- обов'язковий унікальний ідентифікатор для кожного елемента списку
- Не використовуй індекс масиву як key для динамічних списків
- Умовний рендеринг: тернарний оператор,
&&, early return, змінна з JSX - Обережно з
{0 && ...}-- рендерить0 - Умовні CSS-класи: template literals або бібліотека
clsx
Що далі?
Ми вже вміємо будувати інтерактивні інтерфейси. У наступному уроці розберемо обробку подій та роботу з формами -- один з найважливіших аспектів будь-якого вебдодатку.
Корисні посилання:
- React.dev: Rendering Lists -- рендеринг списків та key
- React.dev: Conditional Rendering -- умовний рендеринг
- React.dev: Keeping Components Pure -- чому immutability важливий