open atlas
↑ К треку
Паттерны React RXP · 08 · 01

Эффекты — это про синхронизацию

useEffect синхронизирует React с внешней системой — подписками, браузерными API, не-React-виджетами, соединениями — и очищает их; это не хук «реагируй на состояние» для вычисления производных данных, что и есть источник большинства багов с эффектами.

RXP Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

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

У useEffect ровно одна задача: держать React синхронизированным с системой, живущей вне React. WebSocket, браузерный document.title, библиотека графиков на <canvas>, наблюдатель геолокации. Если в кадре нет внешней системы, шансы подавляюще на то, что эффект тебе вообще не нужен.

Цель

После этого урока ты можешь заявить, что назначение эффекта — синхронизировать React с внешней системой, а не реагировать на изменения состояния; написать корректный эффект, который подписывается на внешнюю систему или настраивает её и разбирает её в функции очистки; распознать доминирующий режим отказа — использование эффекта для вычисления производных данных из пропсов или состояния; и применять senior-эвристику: назови внешнюю систему, с которой синхронизируется эффект, а если не можешь — удали эффект.

1

Эффект — это запасной выход из потока рендеринга React; он существует, чтобы дотянуться до системы, которую React не контролирует. Рендеринг чист: даны пропсы и состояние — ты описываешь UI, а всем остальным владеет React. Но в реальном мире есть вещи, о которых React ничего не знает: сетевые сокеты, document.title, localStorage, императивный экземпляр графика, API IntersectionObserver. Эффект — это санкционированный способ выйти за пределы рендера, тронуть эту внешнюю вещь и держать её в ногу с текущим состоянием твоего компонента.

// каноническая форма: настрой внешнюю систему, верни её разбор
useEffect(() => {
  const connection = createConnection(roomId);   // выходим за пределы React
  connection.connect();
  return () => connection.disconnect();           // разбор при очистке
}, [roomId]);                                      // ре-синхр. при изменении roomId

Читай этот массив зависимостей как «ре-синхронизируй всякий раз, когда меняется roomId», а не «запусти это, когда меняется roomId». Формулировка важна: эффект синхронизирует; он не реагирует.

2

Корректный эффект всегда описывает двусторонний контракт: настрой и разбери. Функция очистки — не опциональная вежливость, это половина паттерна. React может запустить твой эффект, а затем позже перезапустить его (зависимости изменились) или размонтировать компонент, и у каждой настройки должен быть парный разбор, иначе ты протекаешь. Подписка без unsubscribe, setInterval без clearInterval, обработчик события, который так и не сняли, — каждый накапливается по рендерам и перемонтированиям.

function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const socket = new WebSocket(`wss://chat.example.com/${roomId}`);
    const onMessage = (e: MessageEvent) => console.log(e.data);
    socket.addEventListener("message", onMessage);
    return () => {
      socket.removeEventListener("message", onMessage);
      socket.close();                              // разбор точно зеркалит настройку
    };
  }, [roomId]);
  return <div>Room: {roomId}</div>;
}

В Strict Mode из React 19 эффект намеренно запускается как настройка→очистка→настройка в разработке, чтобы немедленно вскрыть отсутствующий разбор. Если твой эффект ломается при этом двойном вызове — очистка неверна; это тест делает свою работу, а не баг, который надо подавить.

3

Синхронизация с браузерным API — тот же паттерн, даже когда это выглядит как «просто выставить значение». Установка document.title из состояния твоего компонента — это синхронизация: document — внешняя система, и ты держишь её в ногу с состоянием React. Этому место в эффекте, и (поскольку заголовок сохраняется) в идеале он восстанавливается при очистке.

function PageTitle({ unread }: { unread: number }) {
  useEffect(() => {
    const previous = document.title;
    document.title = unread > 0 ? `(${unread}) Inbox` : "Inbox";
    return () => { document.title = previous; };   // восстановить при размонтировании
  }, [unread]);
  return null;
}

Признак того, что это настоящий эффект: цель (document.title) — не состояние React, не пропс, не JSX. Ты тянешься за пределы дерева рендера, чтобы мутировать мир. Это и есть вся лицензия на существование эффекта.

4

Доминирующий режим отказа: эффект, используемый для вычисления производных данных из пропсов или состояния. Это то злоупотребление, что производит большинство отчётов «мой эффект глючит». Рассуждение кажется верным — «когда меняется items или query, пересчитай filtered» — поэтому люди пишут эффект, который выставляет второе состояние. Но внешней системы здесь нет. filtered — чистая функция от items и query; вычислять её во время рендера правильно, синхронно и без багов. Версия с эффектом вносит лишний рендер, второй источник истины и окно, в котором экран показывает устаревшие данные.

// НЕВЕРНО: внешней системы нет — это производные данные в костюме эффекта
const [filtered, setFiltered] = useState<Item[]>([]);
useEffect(() => {
  setFiltered(items.filter((i) => i.name.includes(query)));
}, [items, query]);                                // лишний рендер + может показать устаревшее

// ВЕРНО: вычисли во время рендера; ни эффекта, ни второго состояния, никогда не устаревает
const filtered = items.filter((i) => i.name.includes(query));
// (оборачивай в useMemo, только если фильтр действительно дорогой)

Эвристика: прежде чем писать эффект, назови внешнюю систему, с которой он синхронизируется. «Он синхронизируется с… filtered» — это не внешняя система, это состояние React, и эффект надо удалить.

Разбор примера

Живой бейдж «статус онлайн» — сначала ловушка эффекта-как-реакции, затем синхронизация, сделанная правильно. Компонент должен показывать, подключён ли пользователь, где о связности сообщает браузерный navigator.onLine плюс события online/offline — внешняя система.

Частая первая версия мешает настоящую подписку с эффектом производных данных:

function StatusBadge({ user }: { user: { name: string } }) {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [label, setLabel] = useState("");

  // эффект №1: НАСТОЯЩАЯ синхронизация с системой связности браузера
  useEffect(() => {
    const on = () => setIsOnline(true);
    const off = () => setIsOnline(false);
    window.addEventListener("online", on);
    window.addEventListener("offline", off);
    return () => {
      window.removeEventListener("online", on);
      window.removeEventListener("offline", off);
    };
  }, []);

  // эффект №2: НЕВЕРНО — `label` производен от isOnline + user; внешней системы нет
  useEffect(() => {
    setLabel(`${user.name} is ${isOnline ? "online" : "offline"}`);
  }, [isOnline, user.name]);

  return <span>{label}</span>;
}

Эффект №1 образцов: он подписывается на внешнюю систему и очищается. Эффект №2 — это режим отказа: label — чисто функция от isOnline и user.name, так что эффект добавляет рендер и окно устаревания ни за что. Исправление сохраняет одну настоящую синхронизацию и удаляет фальшивую:

function StatusBadge({ user }: { user: { name: string } }) {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const on = () => setIsOnline(true);
    const off = () => setIsOnline(false);
    window.addEventListener("online", on);
    window.addEventListener("offline", off);
    return () => {
      window.removeEventListener("online", on);
      window.removeEventListener("offline", off);
    };
  }, []);

  const label = `${user.name} is ${isOnline ? "online" : "offline"}`;   // производное в рендере
  return <span>{label}</span>;
}

Один эффект, потому что внешняя система ровно одна. Текст бейджа вычисляется во время рендера. (В реальном приложении ты бы потянулся за useSyncExternalStore, чтобы подписаться на navigator.onLine — это следующая шлифовка ровно этого паттерна, и она существует именно потому, что подписка на внешний стор настолько частая.)

Почему это работает

Почему «не используй эффекты для производных данных» сформулировано так абсолютно, когда эффект технически может это сделать? Потому что каждый эффект-как-реакция — это узел в скрытом графе зависимостей: эффект A выставляет состояние, которое триггерит эффект B, который выставляет состояние, которое триггерит эффект C. Каждое звено добавляет проход рендера, такт устаревшего UI и шанс забыть зависимость. Рендеры React дёшевы и синхронны; эффекты асинхронны и упорядочены после отрисовки. Вычисление производных значений в рендере держит всё в одном синхронном проходе с единым источником истины. Эта дисциплина — не догма, это схлопывание хрупкого многопроходного каскада обратно в один проход.

Частая ошибка

Самые коварные случаи прячутся за настоящими внешними системами. «Я делаю запрос в эффекте, а потом в другом эффекте преобразую ответ в состояние отображения». Запрос — законная синхронизация с сетью; эффект-преобразование — та же ошибка с производными данными: преобразуй во время рендера или в самом .then запроса. Именование системы на каждый эффект ловит это: эффект один синхронизируется с сервером; эффект два синхронизируется ни с чем, значит, это не эффект. Один эффект — одна внешняя система. Если единственная задача эффекта — записать состояние React из другого состояния React, это производные данные, и точка.

Проверь себя
Викторина

Ревьюер видит эффект, чьё тело — `setSummary(\`${items.length} items, ${selected.size} selected\`)` с зависимостями `[items, selected]`. И `items`, и `selected` — состояние React. Каков senior-вердикт?

Итог

useEffect — это запасной выход для синхронизации React с внешней системой — подпиской, браузерным API вроде document.title, не-React-виджетом, сетевым соединением, — и у корректного эффекта всегда есть парная очистка, которая разбирает ровно то, что создала его настройка. Читай массив зависимостей как «ре-синхронизируй, когда они меняются», а не «реагируй, когда они меняются». Доминирующий режим отказа и источник большинства багов с эффектами — использование эффекта как общего механизма «реагируй на состояние» для вычисления производных данных: это значение — чистая функция от пропсов и состояния, так что ему место в рендере (или в useMemo, если дорого), а никогда — во втором состоянии, записанном эффектом, что лишь покупает лишний рендер, второй источник истины и устаревший UI. Одна эвристика, которая предотвращает всё это: прежде чем писать эффект, назови внешнюю систему, с которой он синхронизируется; если назвать не можешь — эффект тебе не нужен.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.