Работа с API: загрузка, ошибки, состояние
Загружаем данные с сервера правильно: учитываем три состояния — идёт загрузка, пришли данные, случилась ошибка — и выносим всё в переиспользуемый composable.
Три состояния запроса —
loading(ждём ответ),data(успех) иerror(сбой). Хороший интерфейс показывает каждое из них, а не «зависает» без объяснений.
Наивный и правильный подход
Новичок часто просто кладёт результат fetch в переменную и забывает про загрузку и ошибки. Тогда пользователь видит пустой экран, не понимая, грузится оно или сломалось. Правильно — завести три реактивные переменные и обновлять их по ходу запроса:
<template>
<p v-if="loading">Загрузка...</p>
<p v-else-if="error">Ошибка: {{ error }}</p>
<ul v-else>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const posts = ref([])
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await fetch('https://api.example.com/posts')
if (!res.ok) throw new Error('Статус ' + res.status)
posts.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false // снимаем загрузку в любом случае
}
})
</script>
Связка v-if / v-else-if / v-else из раздела 2 показывает ровно одно из трёх состояний. finally гарантирует, что loading снимется и при успехе, и при ошибке.
Промоделируем три состояния
Логику переходов состояний легко проверить обычным кодом — успех и ошибка ведут к разным финалам, но загрузка снимается всегда:
function loadData(shouldFail) {
const state = { loading: true, data: null, error: null };
try {
if (shouldFail) throw new Error("сеть недоступна");
state.data = ["Пост 1", "Пост 2"];
} catch (e) {
state.error = e.message;
} finally {
state.loading = false;
}
return state;
}
console.log("Успех:", loadData(false));
console.log("Ошибка:", loadData(true));
Вывод:
Успех: { loading: false, data: [ 'Пост 1', 'Пост 2' ], error: null }
Ошибка: { loading: false, data: null, error: 'сеть недоступна' }
Выносим в composable useFetch
Этот паттерн повторяется в каждом запросе, поэтому его выносят в composable из прошлого урока:
<!-- composables/useFetch.js -->
<script>
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
fetch(url)
.then(res => res.json())
.then(json => { data.value = json })
.catch(e => { error.value = e.message })
.finally(() => { loading.value = false })
return { data, loading, error }
}
</script>
Теперь любой компонент пишет лаконично: const { data, loading, error } = useFetch('/api/posts') — и сразу получает все три состояния.
Чек-лист хорошего запроса
| Состояние | Что показать пользователю |
| loading | спиннер или «Загрузка...» |
| error | понятное сообщение и кнопку «Повторить» |
| data (пусто) | «Ничего не найдено», а не пустоту |
| data (есть) | сам список/контент |
Итог
- У запроса три состояния:
loading,data,error— покажите каждое. fetchсasync/awaitзапускают вonMounted;finallyснимает загрузку всегда.v-if/v-else-if/v-elseпоказывают ровно одно состояние.- Повторяющийся паттерн выносят в composable
useFetch.