Как правильно использовать useState и useEffect в кастомном хуке для хранения данных и ошибок
Что проверяют
Умеете ли вы написать хук, который корректно ведёт себя при смене параметров и размонтировании, а не только в счастливом сценарии. Следом обычно спрашивают про гонку запросов и про то, почему в режиме разработки запрос уходит дважды.
Как отвечать
Каркас хука useFetch(url):
- полное состояние: кроме
dataиerrorнуженisLoadingили единое полеstatus. НезависимыеuseStateлегко приходят в невозможную комбинацию, поэтому их обновляют вместе или берутuseReducer; - сброс при новом запросе: в начале эффекта
setError(null)и включение загрузки, иначе ошибка от прошлогоurlвисит поверх новых данных; - отмена: в эффекте создают
AbortController, передаютsignalвfetch, а в очистке вызываютcontroller.abort(). Тогда ответ на старыйurl, пришедший позже нового, не перезапишет данные. ОшибкуAbortErrorв состояние не кладут; - HTTP-ошибки:
fetchне отклоняет промис на 404 или 500, статус проверяют черезresponse.okи бросают исключение сами; - ошибка как объект: хранить
Error, а не одну строку, чтобы компонент отличал сетевой сбой от ответа сервера; - зависимости: в массиве только примитивы или стабильные ссылки. Объект опций, созданный прямо в JSX, новый на каждом рендере и зацикливает эффект;
- что вернуть:
{ data, error, isLoading, refetch }, гдеrefetchобёрнут вuseCallback.
Двойной запрос в разработке это StrictMode: React монтирует, размонтирует и снова монтирует компонент, проверяя очистку эффектов. С abort() в очистке первый запрос просто отменяется, в продакшене повтора нет.
В реальном проекте серверное состояние чаще отдают TanStack Query или SWR: кэш, повторы и дедупликация там уже есть. Самописный хук уместен для простых случаев.
Где спотыкаются
Делают сам колбэк эффекта async. Эффект должен вернуть функцию очистки, а не промис, поэтому асинхронную функцию объявляют внутри и вызывают.
Не обрабатывают гонку. Пользователь быстро переключает вкладки, и на экране оказываются данные предыдущего запроса.
Убирают двойной запрос, выключая StrictMode или ставя флаг в useRef, вместо того чтобы написать нормальную очистку.