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

Общо състояние

Накратко: Общото състояние във Vue се дава с provide/inject или с хранилище в Pinia, а компонентите го четат направо, без props през всички нива.

Проблем

Броячът пак казва 0

Кошницата е повдигната в App, както в урока за общото състояние нагоре. Оттам тя слиза като props: към Header и от него към CartBadge, към Catalog, от него към ProductList и от него към всяка ProductCard. Някой пренаписа Header и броячът спря.

Vue

Добави хляб, мляко и пак хляб. Картите броят, а горе пише „Кошница (0)“. App подава count на Header, но новият Header не го подава на CartBadge. Header не ползва кошницата. Той трябваше само да я предаде нататък и точно това е забравил.

Идеята

Доставчик горе, потребители долу

Подаването през компоненти, които не ползват данните, се казва prop drilling (прокарване на props). При две нива е нормално. При пет всеки компонент по пътя зависи от кошницата, а един пропуснат prop къса веригата.

Двете библиотеки имат начин да дадат данни на далечни потомци без компонентите по средата:

  1. Доставчикът е компонентът, който притежава данните. Той казва: „всичко под мен може да чете това“.
  2. Потребителят е компонентът, който ги ползва. Той ги взима направо, без значение колко нива са между тях.

Във Vue това са provide и inject:

App.vue
<script setup>
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
provide('cart', { items, count, add });
</script>
<!-- CartBadge.vue -->
<script setup>
const { count } = inject('cart');
</script>

provide('cart', …) дава стойност под ключ на всички потомци на компонента. inject('cart') я взима от най-близкия предшественик, който я е дал. Подават се ref-овете (items, count), не стойностите им, затова потребителите остават реактивни.

Данните пак имат един собственик и промените пак минават през него: add е функцията на доставчика. Сменен е само пътят.

Виж го

Виж двата пътя

Отначало кошницата слиза ниво по ниво и веригата се къса по средата. После App става доставчик и двамата потребители четат направо.

<script setup>
const items = ref([]);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0));
function add(product) { /* … */ }
provide('cart', { items, count, add });
</script>
<template>
<Header />
<Catalog />
</template>
<!-- CartBadge.vue -->
<script setup>
const { count } = inject('cart');
</script>
<template>
<a href="/cart">Кошница ({{ count }})</a>
</template>
<!-- ProductCard.vue -->
<script setup>
const { add } = inject('cart');
</script>
<template>
<button @click="add(product)">Добави</button>
</template>
Стъпка по стъпка, с думи
  1. Кошницата живее в App. Трябва на два компонента далеч надолу: CartBadge в горната част и ProductCard в каталога.
  2. Само с props кошницата слиза ниво по ниво. Header, Catalog и ProductList не я ползват. Те само я подават.
  3. Някой пренаписва Header и забравя count. Веригата се къса по средата и броячът показва 0. Това е прокарването на props.
  4. Вместо това: App вика provide('cart', …). Всеки потомък на App може да чете кошницата.
  5. CartBadge и ProductCard я четат с inject('cart'). Компонентите по средата не знаят нищо за кошницата.
  6. Клик на „Добави“ вика add(product). Функцията е дошла от доставчика, затова промяната става в App.
  7. Кошницата в App се сменя и всеки компонент, който я чете, се рисува наново: броячът казва 1, картата казва 1.
  8. Данните пак имат един собственик, App. Сменен е само пътят: не през всяко ниво, а направо от доставчика.

Компонентите по средата вече не знаят нищо за кошницата. Новият Header може да бъде пренаписан колкото искаш.

Пробвай

Махни прокарването

Кошницата слиза като props през Header и Catalog, а Header изпуска count. Дай кошницата отгоре. Header, Catalog и ProductList не получават нищо за кошницата, а CartBadge и ProductCard я четат направо. Проверката добавя хляб, мляко и хляб и гледа картите и брояча.

Vue

Идеята

Хранилище за кошницата

Кошницата расте: добави, махни, смени броя, изпразни. Ако всяко действие е функция в App, App се пълни с логика, която не е за него. Затова общото състояние се изнася в отделен файл, хранилище (store): данните и всичко, което ги сменя, на едно място.

За хранилища Vue ползва Pinia, официалната библиотека за общо състояние. Хранилището се прави с defineStore и се пише като <script setup>: ref е състоянието, computed е изчислено от него, функциите са действията.

cart.js
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
function add(product) { /* … */ }
return { items, count, add };
});

Pinia се включва веднъж, в main.js: createApp(App).use(createPinia()).mount('#app'). Всеки компонент вика useCartStore() и получава едно и също хранилище. Не трябва provide в App.

<script setup>
import { storeToRefs } from 'pinia';
const cart = useCartStore();
const { count } = storeToRefs(cart);
</script>

Хранилището е reactive обект: в шаблона пишеш cart.count, без .value. Ако искаш да го разглобиш на променливи, ползвай storeToRefs(cart). Той дава ref-ове, които остават свързани с хранилището. Действията (cart.add) може да се вземат направо.

Vue
Отвори cart.js: там е цялата логика на кошницата. App.vue вече няма състояние, а main.js включва Pinia.

Пробвай

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

Картите броят, а броячът горе остава „Кошница (0)“. В конзолата няма грешка. Кой ред е виновен?

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

Идеята

Кога общо и кога не

Общото състояние е удобно и затова изкушава всичко да отиде там. Но всяко нещо в него може да се чете и сменя отвсякъде, а това прави грешките по-трудни за намиране. Правилото от урока за повдигането важи: толкова високо, колкото трябва, и не по-високо.

  • Общо: това, което трябва на далечни части на дървото и живее, докато потребителят е на сайта. Кошницата, влезлият потребител, езикът, темата.
  • В компонента: полетата на формуляра, дали менюто е отворено, кой раздел е избран, текстът в търсачката. Те трябват на един компонент (или на родителя му) и изчезват с него.
  • Данните от сървъра не са общо състояние. Списъкът с продукти и поръчките имат собственик и той е сървърът. В хранилището те са копие, което трябва да се опреснява. Компонентът, който ги показва, ги зарежда, както в урока за данните от API.

Общото състояние не сменя посоката: данните тръгват от един собственик, а промените минават през неговите функции.

Пробвай

Попълни

Как CartBadge взима count от хранилището така, че да остане свързан с него?
<script setup>
import { storeToRefs } from 'pinia';
import { useCartStore } from './cart.js';

const { count } = (useCartStore());
</script>
По-точно, за любопитните

Хранилище с опции. defineStore приема и обект: state: () => ({ items: [] }), getters и actions. Това е същото, записано като Options API. Pinia поддържа и двата вида.

provide/inject или Pinia. provide е вграден и е добър за неща, които трябват на едно поддърво (настройките на една таблица, формуляр с вложени полета). Pinia е за състоянието на цялото приложение: видимо е в Vue DevTools, тества се отделно и не зависи от това кой компонент е горе.

Ключ-символ. Вместо низа 'cart' при provide често се ползва Symbol(), изнесен от общ файл. Така два различни доставчика не могат случайно да имат един и същ ключ.

inject със стойност по подразбиране. inject('cart', null) не пише предупреждение, ако никой не е дал ключа, а връща null.

Хранилището живее, докато страницата е отворена. При презареждане се губи. Кошница, която трябва да оцелее, се записва в localStorage или на сървъра.

Проверките виждат поведението. Задачите тук не могат да видят дали кошницата идва от props, от контекст или от хранилище. Те проверяват, че броячът и картите показват едно и също.

Упражнение

Страницата на кошницата

В главната част има CartSummary: заглавие „Кошницата“, редове „Бял хляб × 2“ с бутон „Махни“, „Общо: 4,00 €“ и „Кошницата е празна“, когато е празна. Сега тя пази своя кошница, която никой не пълни. Добави в хранилището махане и обща сума и направи CartSummary да чете кошницата оттам. Проверката добавя три продукта, маха ги и гледа брояча, картите и сумата.

Vue

Тест

Провери се

1. Кошницата слиза от App през Catalog и ProductList до ProductCard. Catalog и ProductList не я ползват. Какво е лошото?
2. CartBadge пише const { count } = useCartStore();. Какво показва броячът?
3. ProductCard вика inject('cart'), а никой над него не е викал provide('cart', …). Какво става?
4. Кое от тези трябва да е в общото състояние?
5. Списъкът с продукти идва от /api/products. Да го сложим ли в общото хранилище?

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