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
:
- выставляет состояние
pending === true
; - делает запрос к api;
- дожидается ответа;
- записывает результат в
response
; - выставляет состояние
pending === false
;
Если вы обратили внимание на пункт №3, то не зря. JavaScript не блокирует поток выполнения программы пока идёт запрос по сети. Соответственно, во время загрузки данных пользователь может что-то сделать и состояние приложения изменится. Например, компонент может быть отмонтирован до того, как мы перешли к шагу №4. И тогда получается, что компонента больше нет в дереве, а мы пытаемся в нём записать различные данные в переменные. Это ошибка.
Как исправить
Исправление довольно простое. Нужно держать в голове две вещи:
- В React всё идёт от состояний
- Состояния изменяются в ответ на эффекты и события
В каком состоянии приложения происходит сетевой запрос? Когда компонент примонтирован, ему доступен 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. О них подробнее я расскажу как-нибудь потом, а самым любопытным вот ссылки:
- State machine
- State charts
- Библиотека, облегчающая работу с состояниями xstate