Гузариш ба мундариҷа
Мавзӯи 5 аз 5
Назария

Эффекты и проект: корзина

Побочные эффекты

Компонент — это функция «данные → разметка». Но приложению нужно делать и другое: менять заголовок вкладки, запускать таймеры, загружать данные с сервера, подписываться на события окна, сохранять в localStorage. Это побочные эффекты: действия, которые выходят за пределы отрисовки. Делать их прямо в теле компонента нельзя — функция-компонент вызывается при каждой перерисовке, и таймер запускался бы сотни раз.

Для эффектов есть хук useEffect: React выполнит переданную функцию после того, как покажет компонент на экране.

useEffect и зависимости

const { useState, useEffect } = React;

function Clicker() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Нажато ${count} раз`;
  }, [count]);

  return <button onClick={() => setCount((c) => c + 1)}>Жми</button>;
}

Второй аргумент — массив зависимостей: от каких значений зависит эффект. React запускает эффект после первой отрисовки, а потом — только когда хотя бы одна зависимость изменилась.

Запись Когда выполняется
useEffect(fn, [a, b]) после первой отрисовки и когда меняется a или b
useEffect(fn, []) один раз — после первой отрисовки
useEffect(fn) после каждой отрисовки (нужно редко)

В зависимости перечисляют все props и состояния, которые используются внутри эффекта.

Очистка

Если эффект что-то запускает (таймер, подписку), он должен уметь это остановить. Для этого эффект возвращает функцию очистки. React вызовет её, когда компонент уберут со страницы, а также перед повторным запуском эффекта:

function Clock() {
  const [now, setNow] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);        // очистка
  }, []);

  return <p>{now.toLocaleTimeString()}</p>;
}

Без очистки после ухода со страницы таймер продолжил бы работать и пытаться обновить несуществующий компонент — это утечка памяти.

Та же схема для событий окна:

useEffect(() => {
  const onResize = () => setWidth(window.innerWidth);
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);

Ловушка старого значения

Функция внутри setInterval создаётся один раз и «видит» значение состояния только из той отрисовки, где её создали. Поэтому внутри таймеров состояние обновляют функцией:

setCount(count + 1);       // внутри интервала count всегда 0 → застрянет на 1
setCount((c) => c + 1);    // всегда берёт актуальное значение

Загрузка данных

Самый частый эффект в реальных приложениях — запрос к серверу при открытии компонента:

function Users() {
  const [users, setUsers] = useState(null);

  useEffect(() => {
    fetch("/api/users")
      .then((r) => r.json())
      .then(setUsers);
  }, []);

  if (users === null) return <p>Загрузка…</p>;
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

На нашем сайте код в песочнице не может обращаться к сети, поэтому этот пример только для чтения.

Нужен ли эффект?

Эффект нужен для связи с внешним миром. Для вычислений он не нужен: сумму корзины, отфильтрованный список, «можно ли отправить форму» считают прямо при отрисовке. Частая ошибка новичков — хранить вычисляемое значение в отдельном состоянии и синхронизировать его эффектом. Это лишняя перерисовка и лишний источник ошибок.

Проект: корзина

Корзина собирает всё из курса:

  • props: список товаров приходит снаружи;
  • списки и key: товары и строки корзины рисуются через map;
  • состояние: в нём только то, что выбрал пользователь, — [{ id, qty }];
  • неизменяемые обновления: добавить — [...cart, ...], изменить — map, удалить — filter;
  • вычисления при отрисовке: название и цена берутся из products по id, сумма — reduce.
const total = cart.reduce((sum, line) => {
  const product = products.find((p) => p.id === line.id);
  return sum + product.price * line.qty;
}, 0);

Почему в корзине не хранить название и цену? Тогда при изменении цены в каталоге корзина показывала бы старую. Храни минимум, остальное вычисляй — это главный принцип состояния в React.

Что дальше

Мы прошли ядро React. Дальше обычно изучают: useRef (ссылка на DOM-элемент), useContext (данные для всего дерева без передачи через props), свои хуки, маршрутизацию (React Router) и фреймворк Next.js. Кстати, этот сайт сделан на React и Next.js.

Итог

  • useEffect(fn, [зависимости]) — действие после отрисовки: таймеры, подписки, запросы, document.title.
  • [] — один раз; зависимости — всё, что используется внутри эффекта.
  • Эффект возвращает функцию очистки — clearInterval, removeEventListener.
  • Внутри таймеров — set((old) => ...). Вычисления — при отрисовке, не в эффектах.

Заголовок вкладки

Осон+10 XP

Напиши компонент TitleCounter: кнопка <button> с текстом Click и счётчик нажатий в состоянии. После каждой отрисовки заголовок вкладки (document.title) должен быть Clicks: N.

Меняй заголовок в эффекте useEffect, с массивом зависимостей.

сначала         → Clicks: 0
после 2 кликов  → Clicks: 2

Барои ҳал кардани масъалаҳо коннекторро пайваст кун

Коди масъалаҳо дар компютери худат иҷро мешавад — ин аз браузер тезтар ва боэътимодтар аст. Барои ин барномаи ройгони коннектори CodeWithMe лозим аст. Онро як бор пайваст кардан кифоя аст.

Коннекторро дар ин компютер ҷустуҷӯ мекунем…

  1. 1
    Коннекторро барои системаи худ (Windows, macOS ё Linux) зеркашӣ кун.Зеркашии коннекторДар саҳифаи зеркашӣ файлеро интихоб кун, ки дар номаш системаи ту ҳаст, масалан windows-amd64.
  2. 2Онро иҷро кун ва ҷузвдонеро интихоб кун, ки ҳалҳоят дар он ҷо меистанд. Python ва Node.js-ро ӯ худаш ҳангоми зарурат ба он ҷо мегузорад.
  3. 3
    Дар поён рамзи 6-рақамаро ворид кун — он дар равзанаи коннектор навишта шудааст («Рамз барои сайти CodeWithMe»). Агар браузер дар бораи дастрасӣ ба шабакаи маҳаллӣ пурсад — иҷозат деҳ.

Коннектор кушода аст (рамзи манбаъ дар GitHub), танҳо дар компютери ту кор мекунад ва кодро танҳо пас аз ворид кардани рамзи пайвастшавӣ иҷро мекунад.