Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Эффекты, ссылки и внешние системы
effects_refs

Эффекты, ссылки и внешние системы

useEffect как синхронизация, зависимости, очистка, ссылки, useEffectEvent, гонки запросов и отмена работы.

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Эффекты, ссылки и внешние системы

Эффект нужен не после каждой отрисовки, а только для синхронизации состояния React с системой вне React.

#Результат урока

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

Понадобятся этапы отрисовки и фиксации, снимки состояния, замыкания и отмена асинхронной работы. Итог урока — виджет живого соединения с корректным жизненным циклом, отменой устаревших запросов и управлением фокусом.

#1. Эффект синхронизирует внешнюю систему

Эффект выполняется после фиксации DOM и приводит внешний ресурс в соответствие текущим свойствам и состоянию: соединение, API браузера, сторонний виджет или подписку. Если значение можно вычислить во время отрисовки либо действие вызвано событием пользователя, эффект не нужен. До написания кода назовите ресурс и способ его освобождения.

Самый частый лишний эффект вычисляет то, что и так выводится из свойств:

// ❌ внешней системы нет: эффект и состояние здесь не нужны const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]); // ✅ производное значение вычисляется во время отрисовки const fullName = `${firstName} ${lastName}`;

Первый вариант даёт лишнюю фиксацию DOM и кадр, в котором fullName ещё пустое. Проверка простая: назовите внешнюю систему. Если её нет — эффект лишний.

Проверьте себя. Для каждого эффекта назовите внешнюю систему; удалите эффект без такой системы.

Частая ошибка. Пересчитывать fullName из firstName и lastName через эффект и состояние.

#2. Подключение и очистка как одна операция

Эффект возвращает функцию очистки, которая отменяет именно выполненное подключение: удаляет тот же обработчик, отписывается, разрывает соединение или очищает таймер. Перед повторным запуском React сначала вызывает прежнюю очистку; при размонтировании — последнюю. Такая симметрия делает повторное монтирование безопасным.

Очистка должна снимать ровно то, что было добавлено, — и это требует одной и той же ссылки на функцию:

// ❌ removeEventListener получает новую функцию: обработчик остаётся навсегда useEffect(() => { window.addEventListener('resize', () => setWidth(window.innerWidth)); return () => window.removeEventListener('resize', () => setWidth(window.innerWidth)); }, []); // ✅ одна ссылка на добавление и снятие useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);

Проверить симметрию можно счётчиком: после смены roomId и размонтирования число активных обработчиков и соединений должно вернуться к нулю. StrictMode делает эту проверку автоматически, повторяя подключение и очистку в разработке.

Проверьте себя. Проверьте число обработчиков после смены roomId и размонтирования.

Частая ошибка. Передать removeEventListener новую функцию, не совпадающую с добавленной.

#3. Реактивные зависимости без ручного выбора

Все реактивные значения, прочитанные эффектом, входят в список зависимостей. Этот список следует из кода, а не задаёт желаемую частоту запуска. Чтобы изменить зависимости, перестройте код: создайте объект внутри эффекта, вынесите константу из компонента, используйте функцию обновления или Effect Event. Отключение правила линтера оставляет устаревшее замыкание (stale closure).

Объект, созданный при отрисовке, — новая ссылка каждый раз, поэтому эффект запускается бесконечно:

// ❌ options новый на каждой отрисовке: эффект перезапускается всегда const options = { serverUrl, roomId }; useEffect(() => { const connection = connect(options); return () => connection.close(); }, [options]); // ✅ объект создаётся внутри эффекта, в зависимостях — первичные значения useEffect(() => { const connection = connect({ serverUrl, roomId }); return () => connection.close(); }, [serverUrl, roomId]);

Отключение правила линтера решает симптом и создаёт худшую проблему: эффект продолжит читать значения первой отрисовки. JSON.stringify(options) в зависимостях тоже не решение — он скрывает настоящую структуру зависимостей и ломается на порядке ключей.

Проверьте себя. Исправьте эффект с объектом в зависимостях без JSON.stringify и отключения линтера.

Частая ошибка. Оставить пустой массив, хотя эффект читает меняющийся roomId.

#4. Гонки запросов и отмена

Несколько асинхронных операций могут завершиться не по порядку. Очистка помечает результат устаревшим или отменяет поддерживаемый fetch через AbortController. Отмена не всегда останавливает работу сервера и может опоздать, поэтому результат всё равно связывают с текущим запросом. Ошибку отмены обычно не показывают пользователю как сбой.

Полная защита состоит из двух независимых механизмов — отмены и проверки актуальности:

// src/features/search/useResults.ts useEffect(() => { const controller = new AbortController(); let active = true; // страховка: отмена может опоздать void (async () => { try { const data = await fetchResults(query, { signal: controller.signal }); if (active) setResults(data); // применяем только результат текущего запроса } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') return; // не сбой if (active) setError(error); } })(); return () => { active = false; controller.abort(); }; }, [query]);

Без флага active возможен сценарий, когда отмена не успела: старый ответ уже пришёл в обработчик, и setResults перезапишет данные нового запроса. Отдельно важно не показывать AbortError как ошибку пользователю — это штатное завершение отменённой работы, а не сбой.

Проверьте себя. Искусственно задержите старый запрос дольше нового и проверьте итог.

Частая ошибка. Применять последний завершившийся ответ независимо от входных данных эффекта.

#5. ref для данных, не влияющих на интерфейс

ref сохраняет значение между отрисовками и не вызывает новую отрисовку при записи. Он подходит для DOM-узла, идентификатора таймера, экземпляра внешнего виджета и других данных, не влияющих на JSX. Не читайте и не изменяйте ref.current во время вычисления компонента, кроме предсказуемой ленивой инициализации.

Идентификатор таймера — типичный пример: он нужен для очистки, но не участвует в разметке.

// src/features/timer/useCountdown.ts const timerRef = useRef<number | null>(null); useEffect(() => { timerRef.current = window.setInterval(() => setLeft(value => value - 1), 1000); return () => { if (timerRef.current !== null) window.clearInterval(timerRef.current); }; }, []);

Обратный случай — статус, который пользователь должен видеть:

// ❌ запись в ref не вызывает отрисовку: интерфейс останется прежним statusRef.current = 'saved'; // ✅ отображаемое значение живёт в состоянии setStatus('saved');

Чтение ref.current во время вычисления компонента тоже нарушает контракт: React может вызвать компонент повторно или отбросить результат, и значение окажется прочитанным из другого кадра.

Проверьте себя. Сохраните идентификатор таймера в ref, очистите его при повторном запуске и размонтировании.

Частая ошибка. Хранить отображаемый пользователю статус только в ref.

#6. useLayoutEffect и измерение разметки

useLayoutEffect запускается после изменения DOM, но до показа кадра, и может синхронно запросить новую отрисовку. Он нужен для измерения расположения и незаметной корректировки. Поскольку такой эффект задерживает кадр, сеть, журналирование и обычные подписки оставляют в useEffect. На сервере измерять разметку нельзя.

Измерение до показа кадра избавляет от заметного «прыжка» подсказки:

// src/shared/ui/Tooltip.tsx useLayoutEffect(() => { const rect = tooltipRef.current?.getBoundingClientRect(); if (!rect) return; // корректируем положение до того, как браузер нарисует кадр if (rect.right > window.innerWidth) setAlign('left'); }, [content]);

С обычным useEffect пользователь успеет увидеть подсказку в неверном положении: кадр уже показан, и исправление произойдёт в следующем. Обратная сторона — блокировка кадра: если поставить в useLayoutEffect сетевой запрос или журналирование, интерфейс подвиснет на время их выполнения. На сервере такой эффект не запускается вовсе, поэтому код в нём не должен быть обязательным для первой отрисовки.

Проверьте себя. Измерьте подсказку и разместите её до показа кадра, затем сравните с обычным эффектом.

Частая ошибка. Заменить все эффекты на useLayoutEffect «для надёжности».

#7. useEffectEvent для нереактивного чтения

Effect Event позволяет эффекту вызвать логику, которая читает последние свойства и состояние, но не должна влиять на повторное подключение. Например, соединение зависит от roomId, а уведомление использует текущую theme. Effect Event вызывают только из эффектов и не передают как обычный обработчик. Настоящую зависимость так скрывать нельзя.

// src/ChatRoom.tsx const onConnected = useEffectEvent(() => { showNotification('Подключено', theme); }); useEffect(() => { const connection = connect(roomId); connection.on('connected', onConnected); return () => connection.close(); }, [roomId]);

Смена theme меняет вид уведомления, но не является причиной переподключения к комнате. Границу легко перейти: если спрятать в Effect Event чтение roomId, эффект перестанет переподключаться при смене комнаты и будет держать соединение с прежней.

// ❌ настоящая зависимость спрятана: соединение останется со старой комнатой const connectToRoom = useEffectEvent(() => connect(roomId)); useEffect(() => { const connection = connectToRoom(); return () => connection.close(); }, []);

Правило разделения простое: значение, определяющее сам ресурс, — зависимость; значение, влияющее только на содержание уведомления, — нереактивное чтение.

Проверьте себя. Смените theme без переподключения и проверьте её значение при следующем событии connected.

Частая ошибка. Убрать roomId из зависимостей, спрятав его чтение в Effect Event.

#8. Проверка эффектов и удаление каскадов

Цепочка «эффект → обновление состояния → эффект → обновление» создаёт лишние фиксации и хрупкие промежуточные состояния. Одно пользовательское событие часто может выполнить все переходы через обработчик или редьюсер. Производные данные вычисляют во время отрисовки. Для каждого оставшегося эффекта назовите внешнюю систему и правило очистки.

Каскад из трёх эффектов заменяется одним переходом состояния:

// ❌ три фиксации и два промежуточных состояния, которых не должно существовать useEffect(() => { if (card) setGoldCount(count => count + 1); }, [card]); useEffect(() => { if (goldCount > 3) setRound(round => round + 1); }, [goldCount]); useEffect(() => { if (round > 5) setGameOver(true); }, [round]); // ✅ одно событие — один согласованный переход function handleCardSelect(nextCard: Card) { dispatch({ type: 'select_card', card: nextCard }); // редьюсер считает всё сразу }

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

Проверьте себя. Сверните каскад выбора карточки и сброса раунда в один переход состояния.

Частая ошибка. Координировать рабочий сценарий наблюдением за состоянием после фиксации DOM.

#Лаборатория

Исправьте чат: соединение пересоздаётся при смене theme, старый запрос перезаписывает новый, обработчик не удаляется, а ref читается во время отрисовки. Примените эффект, очистку, AbortController и useEffectEvent.

Работа готова, когда проверка StrictMode оставляет один ресурс, устаревший ответ не применяется, а зависимости проходят линтер без отключения правил.

#Источники для сверки

  • React: Synchronizing with Effects
  • React: You Might Not Need an Effect
  • React useEffectEvent
  • React useRef

Устранение неисправностей

Далее: Пользовательские хуки