Эффекты и проект: корзина
Побочные эффекты
Компонент — это функция «данные → разметка». Но приложению нужно делать и другое: менять заголовок
вкладки, запускать таймеры, загружать данные с сервера, подписываться на события окна, сохранять в
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) => ...). Вычисления — при отрисовке, не в эффектах.