Как делать сетевые запросы в React

2021.02.01

Senior React Developer

Недавно я наткнулся на код в одном проекте, где компонент делает запрос к серверу для получения необходимых данных:

const [pending, setPending] = React.useState(false) const [response, setResponse] = React.useState(null) const fetch = React.useCallback(async () => { setPending(true) try { // Воображаемая библиотека возвращает нужные данные const response = await api.fetch() setResponse(response) } finally { setPending(false) } }, []) React.useEffect(() => { fetch() }, [fetch])

Этот код написал опытный разработчик, который знает, как работает React и сетевые запросы. И тем не менее, он допустил ошибку. Не удивительно, ведь даже в официальной документации React необычайно мало внимания уделено работе с сетью.

Что не так?

Давайте разберём пошагово, что тут просходит. При вызове функции/компонента создаём переменные для состояния: pending и response с помощью useState. После этого создаётся функция fetch хуком useCallback и далее, в хуке useEffect, мы указываем, что при любом изменении fetch (но не менее одного раза) надо запустить fetch().

Что делает fetch:

  1. выставляет состояние pending === true;
  2. делает запрос к api;
  3. дожидается ответа;
  4. записывает результат в response;
  5. выставляет состояние pending === false;

Если вы обратили внимание на пункт №3, то не зря. JavaScript не блокирует поток выполнения программы пока идёт запрос по сети. Соответственно, во время загрузки данных пользователь может что-то сделать и состояние приложения изменится. Например, компонент может быть отмонтирован до того, как мы перешли к шагу №4. И тогда получается, что компонента больше нет в дереве, а мы пытаемся в нём записать различные данные в переменные. Это ошибка.

Как исправить

Исправление довольно простое. Нужно держать в голове две вещи:

  1. В React всё идёт от состояний
  2. Состояния изменяются в ответ на эффекты и события

В каком состоянии приложения происходит сетевой запрос? Когда компонент примонтирован, ему доступен DOM и состояние компонента "загрузка данных".
В ответ на какое событие мы переключаем состояние загрузки в false? В ответ на окончание загрузки.
И наконец вопрос с подвохом: при каких условиях мы отбрасываем результат?. Дело в том, что мы не можем опираться на состояние компонента "Отмонтирован", потому что после отмонтирования компонента его состояние уничтожается. Значит придётся применить немного JavaScript магии. Вот готовый пример:

const [pending, setPending] = React.useState(false) const [response, setResponse] = React.useState(null) // Запускаем смену состояния когда dom доступен React.useEffect(() => { setPending(true) }, []) // Реагируем на смену состояния React.useEffect(() => { if (pending) { // Магическая переменная, с помощью которой мы узнаем нужен // ли нам результат или уже нет let stillMounted = true const fetch = async () => { // Воображаемая библиотека возвращает нужные данные try { const response = await api.fetch() // Выставляем состояние только если всё ещё примонтированы if (stillMounted) { setResponse() } } finally { // Выставляем состояние только если всё ещё примонтированы if (stillMounted) { setPending(false) } } } // Делаем запрос к серверу fetch() // Просим React при отмонтировании компонента кое-что сделать return () => { stillMounted = false } } }, [pending])

Мы создали переменную внутри нашего useEffect, которая хранит состояние компонента (примонтирован или уже нет) и используем возможность useEffect вызвать так называемую clean-up функцию при отмонтировании, с помощью которой мы и выставляем stillMounted = false. Таким образом, если наш запрос затянется и пользователь перейдёт в другую часть приложения, наша функция увидит, что компонент уже отмонтирован.

Заключение

Работа с асинхронным кодом — сложна и для опытных разработчиков. В таких условиях хорошо себя показывают state machine и state charts. О них подробнее я расскажу как-нибудь потом, а самым любопытным вот ссылки:

  1. State machine
  2. State charts
  3. Библиотека, облегчающая работу с состояниями xstate
При копировании материалов нужно обязательно указать авторство (имя и ссылка на сайт).
контакты