Чем стоит вам использование useEffect для загрузки данных и что меняет TanStack Query
Почему загрузка данных на основе эффектов приводит к дублирующимся запросам, шаблонному коду и устаревшим замыканиям в React, и как ключи, параметр staleTime и механизм аннулирования в TanStack Query решают эти проблемы.
Источники: Stop Using useEffect for API Calls, Use React Query Instead
Загрузка данных внутри useEffect — это первый шаблон, который изучают большинство разработчиков React, и он хорошо работает для одного запроса на одной странице. Однако по мере роста приложения этот же шаблон тайно увеличивает количество запросов, повторяет шаблонный код в каждом компоненте и приводит к скрытым ошибкам зависимостей. В этой статье объясняется, откуда возникают эти проблемы, и как TanStack Query (ранее React Query) решает каждую из них, чтобы вы могли решить, когда стоит перейти на него и что настроить после этого.
Шаблон, с которого начинают все
Знакомая структура представляет собой эффект с пустым массивом зависимостей, который вызывает функцию fetch при монтировании компонента:
useEffect(() => {
fetchData();
}, []);
На первый взгляд всё кажется простым, но именно в этом и заключается часть проблемы. В коде не упоминается ничего о кэшировании, удалении дубликатов, повторных попытках, отмене операций или о том, что должно происходить при повторной загрузке компонента. Каждый из этих пробелов превращается в баг или дополнительный запрос, когда количество конечных точек становится десятками, а также появляется множество состояний загрузки и ошибок, которые необходимо отслеживать.
Почему запрос отправляется дважды и какова настоящая причина
Частым первым сюрпризом является обнаружение одинакового запроса дважды в вкладке сети при загрузке страницы. В режиме разработки StrictMode в React намеренно загружает, снимает и повторно загружает компоненты, чтобы выявить эффекты, которые не очищаются автоматически. Дублирование запросов является намеренным в режиме разработки и не происходит при сборке для продакшена.
Двойные запросы являются признаком проблемы, имеющей реальное значение в производственных условиях: запросы, основанные на эффектах, связаны с жизненным циклом компонента. Каждая перезагрузка компонента запускает новый запрос, даже когда данные уже доступны. Компоненты, которые перезагружаются многократно, такие как вкладки, модальные окна и строки списков, постоянно обращаются к бэкенду, увеличивая нагрузку на сервер и делая интерфейс медленнее, чем это необходимо.
TanStack Query разделяет данные и компонент. Результаты хранятся в общем кэше по ключу запроса. Когда другой компонент или тот же самый после перезагрузки запрашивает ключ, данные по которому все еще считаются свежими, библиотека возвращает значение из кэша без обращения к сети. Несколько компонентов, запрашивающих один и тот же ключ в одно и то же время, также делятся одним запросом к серверу.
Управление состоянием загрузки и ошибок вручную
С использованием useEffect каждый запрос требует собственной тройки переменных состояния: для данных, флага загрузки и информации об ошибках:
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
Если повторять это для каждого вызова API, получится множество практически идентичных фрагментов кода, каждый из которых может содержать ошибки. Одной из распространённых проблем является забывание сбросить значение loading при возникновении ошибки, из-за чего индикатор загрузки продолжает работать бесконечно. Другие проблемы включают отсутствие игнорирования ответов, приходящих после того, как компонент уже демонтирован, а также ситуацию, когда медленный ответ при быстрых изменениях входных данных перезаписывает более свежий ответ.
useQuery напрямую возвращает data, error, isLoading и isError (а также более детальные поля статуса), к тому же он сам обрабатывает случаи неправильного порядка ответов. Шаблонный код исчезает из компонентов, а крайние случаи решаются один раз в хорошо протестированной библиотеке, а не многократно в коде приложения.
Массивы зависимостей против ключей запросов
Эффекты зависят от правильного формирования массива зависимостей. Если пропустить значение, которое используется эффектом, возникает устаревшая замыкание, которая отправляет запросы с устаревшими параметрами — это ошибка, которую может потребоваться часы на поиск. Если включить объект или функцию, которые создаются заново при каждой отрисовке, эффект будет выполняться при каждой отрисовке, отправляя запрос каждый раз.
TanStack Query заменяет этот процесс угадывания более чётким контрактом: вы указываете для каждого запроса ключ и функцию загрузки. Ключ должен включать все переменные, от которых зависит запрос, например имя ресурса вместе с идентификатором пользователя или фильтром. Когда ключ меняется, выполняется новый запрос; если он остаётся прежним, используются данные из кэша. Ключи сравниваются по своему содержимому, а не по ссылке, поэтому создание нового массива при каждой отрисовке не приводит к дополнительным запросам. Перезагрузка данных осуществляется на основе явных правил, таких как монтирование компонента, возвращение фокуса окну или повторное подключение к сети, в сочетании с параметром staleTime, который вы настраиваете.
Вам всё равно придётся учитывать зависимости, но вы указываете их один раз в ключе вместо того, чтобы поддерживать массив, который должен синхронизироваться с телом эффекта.
Кэш, staleTime и аннулирование
Три фактора определяют основную часть различий в повседневной работе.
Кэш
После загрузки данных по ключу они остаются в кэше. Когда пользователь уходит и возвращается, результат из кэша отображается мгновенно; если данные устарели, библиотека загружает их заново в фоновом режиме и обновляет экран по прибытии нового ответа.
staleTime
staleTime определяет, как долго данные считаются свежими. Пока они свежие, повторная загрузка и новые подписчики совсем не вызывают запросов. По умолчанию этот показатель равен нулю, что означает, что данные считаются устаревшими сразу же; данные из кэша всё равно отображаются мгновенно, но повторная загрузка происходит при следующей загрузке страницы или при фокусе на окне. Установка разумного значения staleTime для данных, которые не меняются каждую секунду, часто позволяет значительно снизить нагрузку на сервер.
invalidateQueries
После изменения, такого как обновление профиля или создание поста, вы вызываете queryClient.invalidateQueries с соответствующим ключом. Соответствующие запросы отмечаются как устаревшие, а все те, которые в настоящее время используются, автоматически перезагружаются. После каждого изменения не требуется вручную выполнять повторную загрузку, а также не нужно вручную копировать ответы сервера в локальное состояние.
Вместе эти три элемента устраняют большинство проблем, связанных с загрузкой данных на основе эффектов. Чтобы узнать больше о том, как организовывать ключи, параметры запросов и изменения в масштабе, ознакомьтесь с нашим руководством по структурированию слоя данных TanStack Query.
Типичный сценарий миграции
Рассмотрим панель управления SaaS на базе Next.js с несколькими вкладками, каждая из которых загружает собственные данные с помощью useEffect. Поскольку при смене вкладки компоненты перезагружаются, каждая такая смена вызывает новую волну запросов. Бэкенд получает гораздо больше трафика, чем необходимо, и пользователи замечают медленную загрузку.
Миграция обычно включает три шага:
- Заменить каждый эффект на
useQueryвместе с ключом, который фиксирует все параметры запроса. - Указать для каждого запроса значение
staleTime, отражающее частоту изменения его данных. - После каждой операции изменения аннулировать ключи, данные которых затронуты.
Ожидаемым результатом является более плавная работа панели управления, меньшее количество запросов к бэкенду и улучшение пользовательского опыта при одновременном сокращении объема кода в каждом компоненте.
Где всё ещё применим useEffect
Это не повод полностью удалять useEffect. Он по-прежнему остается подходящим инструментом для синхронизации с элементами вне React, которые не являются данными типа запрос-ответ: подписки, обработчики событий DOM, таймеры и внешние виджеты. Однако для API-запросов в развивающемся приложении более подходит специализированная библиотека для управления состоянием сервера, поскольку она снижает нагрузку на бэкенд, устраняет шаблонный код и предоставляет функции кэширования и аннулирования данных из коробки.
Есть и случаи, когда библиотека оказывается избыточной, например, для небольшого приложения с одним-двумя запросами. В таких случаях может хватить небольшого пользовательского хука с правильной функцией отмены; наша статья о минимальном хуке useFetch и его ограничениях рассматривает этот компромисс. Если вы не уверены, сначала мигрируйте одну страницу и сравните количество запросов в вкладке «Сеть».
Основные выводы
- Двойной запрос во время разработки возникает из-за StrictMode; проблема в продакшене заключается в том, что запросы, основанные на эффектах, повторяются при каждой перезагрузке.
- Ключ запроса заменяет массив зависимостей: включайте все параметры, от которых зависит запрос, и на этом основании будет определяться кэширование и повторный запрос.
- Значение
staleTimeпо умолчанию равно нулю, поэтому если вы хотите избежать фоновых повторных запросов при перезагрузке, укажите его явно.
useEffect для синхронизации с внешними системами, а не для загрузки данных с сервера.