Перейти к содержанию

Эффекты в React: зависимости и обновление данных

Каталог уже реагирует на кнопки и передаёт события между компонентами. Добавим небольшую связь с браузером: заголовок вкладки будет отражать выбранную тему. Это пример эффекта. Изменение document.title происходит вне возвращаемого JSX, поэтому его следует отделить от обычного вычисления разметки.

Массив записей остаётся локальным, а скрытие сохраняет смысл предыдущего урока. Новое состояние topic определяет фильтр и подпись вкладки. Важно не переносить фильтрацию в эффект: она просто вычисляет результат из текущих данных и не взаимодействует с внешней системой.

Рендер и синхронизация решают разные задачи

Вызов компонента вычисляет, что должно быть отображено. В строке visible = notes.filter(...) нет побочного изменения браузера или сервера. При одинаковых notes и topic мы получим одинаковый набор записей. Такое вычисление естественно оставить непосредственно в теле функции.

Присваивание document.title меняет объект, принадлежащий браузеру. Если делать это при каждом вызове компонента, вычисление интерфейса получит внешний побочный эффект. React может вызывать компонент повторно, и количество вызовов не служит обещанием количества пользовательских действий. Для синхронизации используем useEffect.

Полный файл src/pages/Home.jsx:

import {useEffect, useState} from 'react';
import {initialNotes} from '../data/notes.js';
import LessonList from '../components/LessonList.jsx';
export default function Home() {
  const [notes, setNotes] = useState(initialNotes);
  const [topic, setTopic] = useState('all');
  useEffect(() => {
    const previous = document.title;
    document.title = `Учебный каталог: ${topic}`;
    return () => {document.title = previous;};
  }, [topic]);
  const visible = notes.filter(note => topic === 'all' || note.topic === topic);
  function hideNote(id) {setNotes(current => current.filter(note => note.id !== id));}
  return <section id="catalog"><h1>Учебный каталог</h1>
    <div className="filter">{['all', 'html', 'css', 'javascript'].map(value =>
      <button key={value} type="button" aria-pressed={topic === value} onClick={() => setTopic(value)}>{value}</button>)}</div>
    <LessonList title="Моя подборка" notes={visible} onHide={hideNote} />
    <button type="button" onClick={() => setNotes(initialNotes)}>Восстановить подборку</button>
  </section>;
}

Эффект запоминает прежний заголовок, устанавливает свой и возвращает функцию очистки. Очистка восстанавливает прежнее значение, когда эта синхронизация заканчивается. В нашем проекте заголовком управляет один такой компонент. Если несколько независимых компонентов станут одновременно менять document.title, понадобится общий порядок владения, иначе их восстановления начнут мешать друг другу.

Что означает массив зависимостей

Внутри эффекта используется topic. Поэтому зависимостью указано [topic]. Когда React завершает обновление с новым значением темы, старый эффект очищается, а новый получает актуальный topic. При локальном скрытии карточки тема не меняется, и новый заголовок не требуется.

Массив notes в эффекте не читается. Добавлять его «на всякий случай» означало бы запускать синхронизацию и после действий, которые не меняют её результат. С другой стороны, если заголовок начнёт включать notes.length, это значение станет частью прочитанных данных и потребуется корректно описать зависимость.

Пустой массив не является командой «исполнить ровно один раз во всех обстоятельствах». Он означает отсутствие реактивных зависимостей у данной синхронизации. Размонтирование и новое монтирование создают новую жизнь компонента, а режим разработки StrictMode дополнительно проверяет очистку. Именно поэтому мы не строим логику на точном числе запусков.

Изменение Что читает эффект Нужно обновить заголовок
all → css Новый topic Да
Скрытие HTML при all topic остаётся all Нет
Восстановление подборки topic не изменился Нет
Уход со страницы Синхронизация заканчивается Нужна очистка

Практический вопрос для зависимостей звучит так: от каких значений зависит связь с внешней системой? Это точнее, чем желание подавить повторные вызовы. Документация рассматривает такой подход в разделе синхронизации эффектов.

Почему StrictMode не следует отключать ради эффекта

В начальном проекте корневой компонент обёрнут в StrictMode. В разработке React выполняет дополнительную последовательность настройки и очистки эффекта, чтобы выявить несогласованное освобождение ресурсов. Наш заголовок после такой проверки должен остаться соответствующим текущей теме, а не накапливать изменения.

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

Неправильная реакция — завести флаг «уже запускалось» и пропустить вторую настройку. Это может скрыть ошибку очистки вместо исправления. Столь же опасно переносить отправку формы в эффект только ради удобства: монтирование компонента не является намерением пользователя создать запись. POST останется обработчиком submit в отдельном уроке.

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

Как не получить лишнее состояние

Иногда фильтр начинают писать через useEffect: при изменении topic вычисляют visible и сохраняют его вторым setter. Тогда состояние notes и состояние visible могут временно разойтись. Появляются дополнительные обновления и вопрос, какое значение считать главным.

Наш visible вычисляется при каждом рендере из двух владельцев данных: notes и topic. При скрытии записи новый массив сразу участвует в фильтре. При смене темы тот же массив даёт другую подборку. Никакой отдельной синхронизации между массивами не требуется.

Другой неправильный вариант — вызывать setTopic внутри эффекта, который зависит от topic, без условия завершения. Получается цепочка: изменение запускает эффект, эффект снова изменяет значение. Если каждое обновление создаёт новое значение, такая цепочка может продолжаться. Для переключения темы достаточно события кнопки.

При добавлении другого эффекта каждый из них должен владеть собственной очисткой. Например, подписка на событие браузера требует удалить именно созданный обработчик, а не все события похожего названия. Не следует складывать несвязанные задачи в один эффект только потому, что они происходят на одной странице. Зависимости заголовка и будущего запроса каталога будут различаться, поэтому у них появятся разные границы жизни.

Как читать ожидаемое поведение

В начальном режиме all подпись вкладки должна содержать «Учебный каталог: all». После выбора css список ограничивается карточкой сетки, а подпись становится «Учебный каталог: css». Английские значения здесь являются внутренним словарём примера; отдельные русские подписи интерфейса можно добавить позднее без изменения идентификаторов тем.

Если убрать css-layout при выбранной теме css, подборка станет пустой, но заголовок останется про css. Это правильно: он описывает выбранный фильтр, а не наличие записи. Неверным было бы автоматически менять тему из-за пустого результата, поскольку пользователь такого действия не совершал.

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

Оглавление серии