Vue
Загрузка данных
Список заказов грузится прямо в компоненте: fetch в onMounted, результат в ref, и всё работает ровно до первого реального сценария. Запрос упал, а спиннер крутится вечно. Пользователь переключил вкладку до ответа, и устаревший ответ затёр свежий. Логику фетча скопировали в три компонента, и теперь правка заголовков идёт в трёх местах. Загрузка данных это не одна строка fetch, а связка loading, error, данные и refetch, которую выносят в композабл и переиспользуют.
- Список заказов: useOrders() инкапсулирует загрузку, loading-спиннер и кнопку повтора при ошибке
- Профиль пользователя: axios-инстанс с базовым URL и токеном в одном месте, а не в каждом компоненте
- Поиск с фильтрами: при смене параметров запрос перезапускается, старый ответ отбрасывается
- Дашборд: несколько композаблов грузят виджеты независимо, каждый со своим loading и error
- Лента: пагинация через refetch с новым курсором без дублирования логики запроса
Предварительные знания
- Реактивность: ref для хранения данных и состояния загрузки
- watch и watchEffect для реакции на смену параметров
- Композаблы: функция use*, возвращающая реактивное состояние и методы
Фетч внутри композабла
Запрос данных, написанный прямо в компоненте, привязывает логику к одному месту и копируется при повторном использовании. Композабл use* выносит фетч в переиспользуемую функцию, которая возвращает реактивное состояние. Компонент становится тонким: вызвал композабл, отрисовал результат.
fetch не считает HTTP-статусы 4xx и 5xx ошибкой: промис резолвится, и нужно вручную проверять response.ok. Без этой проверки сервер вернул 500, а код посчитал запрос успешным и попытался распарсить тело ошибки как данные.
Композабл сам не решает когда грузить. Он отдаёт refetch, а вызов в onMounted, по клику или при смене параметра остаётся за компонентом. Так один композабл подходит и для авто-загрузки, и для загрузки по действию.
Зачем загрузку данных выносят в композабл use* вместо запроса прямо в компоненте?
Состояние loading, error и refetch
Полезный композабл фетча отдаёт четыре вещи: data, loading, error и refetch. loading управляет спиннером и блокировкой кнопок, error даёт сообщение и кнопку повтора, refetch перезапускает запрос. Этот контракт делает обработку каждого состояния в шаблоне явной.
| Состояние | loading | error | data |
|---|---|---|---|
| Идёт запрос | true | null | прежнее или пустое |
| Успех | false | null | ответ сервера |
| Ошибка | false | объект Error | прежнее |
error обнуляют в начале каждого запроса, а loading выставляют в false в блоке finally. Иначе после неудачного запроса error останется висеть при следующей удачной загрузке, а упавший запрос навсегда оставит спиннер крутиться.
Почему loading сбрасывают в false именно в блоке finally?
fetch против axios
fetch встроен в браузер, не требует зависимостей и достаточен для простых случаев, но ручной работы больше: проверка response.ok, явный вызов res.json(), отдельная настройка заголовков на каждый запрос. axios это библиотека с перехватчиками, базовым URL, авто-парсингом JSON и тем, что 4xx/5xx уже считаются ошибкой и попадают в catch.
| Критерий | fetch | axios |
|---|---|---|
| Зависимость | Встроен в браузер | Внешний пакет |
| 4xx/5xx как ошибка | Нет, проверяют response.ok | Да, попадает в catch |
| JSON | res.json() вручную | Парсится автоматически |
| Перехватчики | Нет | Request/response interceptors |
| Базовый URL и таймаут | Вручную | Через axios.create |
Выбор не идеологический. Для пары запросов без общих заголовков fetch экономит зависимость. Когда нужны единый базовый URL, токен в каждом запросе, единая обработка 401 и таймауты, axios-инстанс с перехватчиками убирает копипасту.
Чем поведение fetch отличается от axios при HTTP-статусе 500?
Где загрузке данных место и гонки запросов
Логика запроса живёт в композабле, вызов запускает компонент в нужный момент: onMounted для авто-загрузки, по клику для действия, через watch при смене параметров. Когда параметры меняются часто (поиск, фильтры), возникает гонка: ответ на старый запрос приходит позже ответа на новый и затирает свежие данные. Это решают отменой устаревшего запроса через AbortController.
AbortError это ожидаемое прерывание, а не настоящая ошибка. Его отличают по name === 'AbortError' и проглатывают, иначе отмена устаревшего запроса покажет пользователю фальшивое сообщение об ошибке.
Как предотвращают гонку, когда параметры запроса меняются быстрее, чем приходят ответы?
Связь с другими темами
Урок про ручную загрузку данных. Дальше курс даёт декларативные и готовые инструменты:
- Suspense и асинхронные компоненты — Декларативная граница загрузки вместо ручного отслеживания loading
- VueUse — useFetch как готовая реализация того же паттерна loading/error/refetch
Итог
- Загрузку данных выносят в композабл use*, чтобы переиспользовать запрос с его состоянием в разных компонентах
- Минимальный контракт композабла: data, error, loading и функция refetch для повторного запроса
- fetch встроен в браузер и требует ручной проверки response.ok; axios даёт перехватчики, базовый URL и авто-JSON
- Гонки запросов решают через AbortController или проверку актуальности: устаревший ответ не должен затирать свежий
- Компонент остаётся тонким: он вызывает композабл и рисует состояние, а вся логика запроса живёт в композабле
Связанные уроки
- vue-31-suspense-async — Suspense даёт декларативную обработку асинхронной загрузки на уровне границы вместо ручного loading-флага
- vue-34-vueuse — VueUse предлагает готовый useFetch, который инкапсулирует тот же паттерн loading/error/refetch