Към съдържанието
wannacode

Данни от API в компонент

Накратко: Данните от API във Vue компонент се зареждат от watch със състояние „зарежда“, „грешка“ или „готово“, а AbortController в почистването спира остарялата заявка.

Проблем

Под „Напитки“ има ябълки

Каталогът взима продуктите на категорията от API-то. Тук сървърът отговаря на „Плодове и зеленчуци“ за 1,5 секунди, а на другите категории за 0,3.

Vue

Натисни „Плодове и зеленчуци“ и веднага „Напитки“. Появяват се напитките. Секунда по-късно на тяхно място идват ябълки, банани и домати, а заглавието и натиснатият бутон пак казват „Напитки“. Купувачът вижда грешни продукти и не знае защо.

Идеята

Заявката е ефект

Заявката излиза от компонента към сървъра. Затова е страничен ефект и мястото ѝ е в ефекта, с категорията като зависимост: всяка нова категория пуска нова заявка.

Състоянията са тези от урока за fetch в страницата: зарежда, грешка, готово. Тук те са едно състояние, status, а компонентът рисува клона, който му отговаря, като в урока за условното показване.

watch(() => props.categoryId, async (categoryId) => {
status.value = 'loading';
try {
const response = await fetch(`/api/products?category=${categoryId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
products.value = await response.json();
status.value = 'ready';
} catch (e) {
status.value = 'error';
}
}, { immediate: true });

Функцията на watch може да е async. { immediate: true } зарежда първата категория още при създаването на компонента, а после всяка смяна на categoryId пуска нова заявка.

status става 'loading' в началото на всяко зареждане, а не само първия път. Иначе при смяна старият списък стои под новото заглавие, докато дойде отговорът.

Пробвай

Зареди категорията

Catalog има състоянията products и status, но нищо не ги пълни. Зареди продуктите на categoryId от /api/products?category=… и покажи „Зареждане…“ при всяко зареждане. Проверката гледа млечните (5 продукта), после натиска „Напитки“ и чака „Зареждане…“ и 4 продукта.

Vue

Идеята

Надбягването

Всяко натискане сменя категорията и пуска ефекта наново. Всяко пускане праща своя заявка и чака своя отговор. Нито едно не знае за другите.

Отговорите не идват в реда на заявките. Сървърът може да търси плодовете по-дълго, мрежата може да забави едната. Затова, когато купувачът превключва бързо, две заявки са на път едновременно. Печели тази, която отговори последна, а не тази, която е тръгнала последна.

Това се казва надбягване (race condition). То не личи, докато сървърът е бърз и еднакъв. Личи при бавна мрежа, на телефон, при натоварен сървър.

Виж го

Виж две заявки на път

Лентите растат, докато заявката пътува. Първо без почистване, после с него.

watch(() => props.categoryId, async (categoryId, oldId, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
status.value = 'loading';
try {
const url = `/api/products?category=${categoryId}`;
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
products.value = await response.json();
status.value = 'ready';
} catch (e) {
if (e.name === 'AbortError') return;
status.value = 'error';
}
}, { immediate: true });
Стъпка по стъпка, с думи
  1. Купувачът натиска „Плодове и зеленчуци“. Категорията се сменя, ефектът тръгва и праща заявка. Тази е бавна.
  2. Преди тя да отговори, купувачът натиска „Напитки“. Ефектът тръгва пак и праща втора заявка. Сега две заявки пътуват едновременно.
  3. Бързата заявка отговаря първа: списъкът показва напитките.
  4. Да кажем, че почистването липсва. Бавната заявка отговаря последна, нейното пускане също слага продуктите и под „Напитки“ има плодове.
  5. Сега с почистването. Всяко пускане на ефекта прави свой AbortController и подава неговия signal на fetch.
  6. Новата категория вика почистването на старото пускане: abort() спира бавната заявка. Нейният fetch хвърля AbortError, а catch го пропуска: това не е грешка.
  7. Отговаря само заявката за напитките. Списъкът и заглавието съвпадат.
  8. Правилото: заявка в ефект има отказ в почистването. Последното пускане печели, защото всички преди него са спрени.

Без почистване екранът показва последния отговор. С почистване старата заявка е спряна, преди да е отговорила, и последният отговор е винаги на последната категория.

Идеята

Отказ в почистването

Ефектът вече има място за „спри старото“: почистването. Браузърът има инструмент за спиране на заявка: AbortController.

  1. Всяко пускане на ефекта прави свой controller = new AbortController().
  2. Подава неговия controller.signal на fetch: fetch(url, { signal: controller.signal }).
  3. Почистването вика controller.abort(). Заявката спира, а await fetch(…) хвърля грешка с име AbortError.
  4. catch пропуска тази грешка: if (e.name === 'AbortError') return;. Заявката не е паднала, кодът сам я е спрял.
watch(() => props.categoryId, async (categoryId, oldId, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
status.value = 'loading';
try {
const response = await fetch(url, { signal: controller.signal });
…
} catch (e) {
if (e.name === 'AbortError') return;
status.value = 'error';
}
}, { immediate: true });

onCleanup е преди първия await. Ако е след него, почистването се записва чак когато отговорът е дошъл, и вече няма какво да спре.

Почистването върви и когато компонентът си отиде. Купувачът е излязъл от каталога: заявката спира и никой не слага продукти в компонент, който го няма.

Пробвай

Спри старата заявка

Направи така, че при бързо превключване старата заявка да спре. Проверката натиска „Плодове и зеленчуци“ и веднага „Напитки“, чака „Зареждане…“ и напитките, изчаква бавния отговор и гледа, че ябълки няма, а съобщение за грешка не се е появило.

Vue

Пробвай

Намери грешката

Купувачът превключва бързо и за миг вижда „Продуктите не се заредиха.“, после идват продуктите. Сървърът не е върнал грешка. Кой ред е виновен?

Избери реда с грешката.

Пробвай

Попълни

Какво спира старата заявка?

watch(() => props.categoryId, (categoryId, oldId, onCleanup) => {
  const controller = new AbortController();
  onCleanup(() => controller.());
  fetch(url, { signal: controller.signal }).then(show);
}, { immediate: true });

Идеята

Грешка и „Опитай пак“

Сървърът понякога отговаря с 500. Проверката на response.ok хвърля, catch слага 'error', а компонентът показва съобщението с role="alert". Купувачът трябва да може да опита пак, без да презарежда страницата.

Опитът наново е същото зареждане. Ефектът вече го прави, когато се смени зависимост. Затова „Опитай пак“ сменя зависимост: число attempt, което само брои опитите.

const attempt = ref(0);
watch([() => props.categoryId, attempt], async ([categoryId], old, onCleanup) => {
// … същото зареждане
}, { immediate: true });
<button type="button" @click="attempt++">Опитай пак</button>

Източникът е масив: watch тръгва, когато се смени кое да е от двете. Функцията получава масив със стойностите.

Новото пускане слага 'loading', така че съобщението и бутонът изчезват, докато заявката пътува.

По-точно, за любопитните

Библиотеки за данни. В истински проект зареждането рядко се пише на ръка във всеки компонент. Има библиотеки, които правят това и още: пазят отговорите в кеш, не пращат две еднакви заявки, опитват пак сами, обновяват данните, когато купувачът се върне на раздела. За Vue това са useFetch от VueUse, Pinia Colada и TanStack Query за Vue. Те не са в пясъчника и курсът няма уроци за тях. Под капака правят същото като тук: заявка в ефект, състояния, отказ.

Флаг вместо отказ. Вместо AbortController почистването може да вдигне флаг: let ignore = false; в ефекта, ignore = true в почистването и if (!ignore) преди да се сложат продуктите. Старата заявка пак стига до сървъра, но отговорът ѝ се изхвърля. Отказът спира и самата заявка. Двата начина решават надбягването.

Vue и отказът. Наблюдателят от <script setup> спира сам, когато компонентът си отиде, и вика последното onCleanup. Затова заявка, която още пътува, се отказва и при излизане от страницата.

Упражнение

Опитай пак

Тук първата заявка за млечните получава 500. Каталогът показва „Продуктите не се заредиха.“, но няма как да опиташ пак. Добави бутон „Опитай пак“, който пуска зареждането наново. Отказът при бързо превключване трябва да остане. Проверката чака грешката, натиска „Опитай пак“, чака „Зареждане…“ и 5 продукта, после превключва бързо на плодовете и напитките.

Vue

Тест

Провери се

1. Купувачът натиска „Плодове и зеленчуци“, после „Напитки“. Отговорите идват в обратен ред. Какво показва код без почистване?
2. Защо наблюдателят на категорията е с { immediate: true }?
3. Защо catch пропуска AbortError, вместо да покаже „Продуктите не се заредиха.“?
4. Как „Опитай пак“ пуска заявката наново?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·