Вивчай

Списки та умовний рендеринг

React списки та умовний рендеринг — меню-дошка в кафе де деякі страви мають мітку sold-outReact списки та умовний рендеринг — меню-дошка в кафе де деякі страви мають мітку 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

Що далі?

Ми вже вміємо будувати інтерактивні інтерфейси. У наступному уроці розберемо обробку подій та роботу з формами -- один з найважливіших аспектів будь-якого вебдодатку.

Інфо

Корисні посилання: