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

Вдигане на състоянието

Накратко: Вдигането на състоянието (lifting state up) мести общото състояние в най-близкия общ родител: данните слизат като props, а промените се качват със събитие (emit).

Проблем

Броячът горе не знае

Магазинът има брояч на кошницата горе и списък с продукти долу. Добави хляб, мляко и още един хляб.

Vue

Картата на хляба казва „В кошницата: 2“, а горе пише „Кошница (0)“. Кошницата живее в ProductList: там е натиснат бутонът и там е добавено състоянието. CartBadge е друг компонент, брат на ProductList, и има свое число, което никой не сменя. Двата компонента имат две кошници и всеки е прав за своята.

Идеята

Нагоре до общия родител

Компонентът вижда само своето състояние и props, които му подава родителят. Брат не вижда брат си. Затова две места, които показват една кошница, не може всяко да има своя.

Решението е да качиш състоянието в най-близкия общ родител на двамата. Тук това е App. Това се казва повдигане на състоянието (lifting state up):

  1. App пази items и изчислява count.
  2. Данните слизат като props: count към CartBadge, items към ProductList.
  3. Промените се качват като съобщение: бутонът в ProductCard не сменя кошницата, а казва на App „добави този продукт“.
<script setup>
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
function add(product) { /* сменя items */ }
</script>
<template>
<header><CartBadge :count="count" /></header>
<main><ProductList :products="PRODUCTS" :items="items" @add="add" /></main>
</template>

Детето обявява какви събития изпраща с defineEmits(['add']) и ги изпраща с emit('add', product). Родителят ги слуша с @add="add", както слуша @click на бутон.

Сега CartBadge и ProductList нямат свое състояние за кошницата. Те показват каквото им даде App.

Виж го

Виж как се качва

Отначало кошницата е в ProductList. После отива в App и оттам тече надолу.

App.vue
<script setup>
const items = ref([]);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0),
);
function add(product) {
plusOne(items.value, product);
}
</script>
<template>
<CartBadge :count="count" />
<ProductList :items="items" @add="add" />
</template>
<!-- ProductList.vue -->
<ProductCard v-for="p in PRODUCTS" :key="p.id" :product="p"
:qty="qtyOf(items, p)" @add="(product) => emit('add', product)" />
<!-- ProductCard.vue -->
<button @click="emit('add', product)">…</button>
Стъпка по стъпка, с думи
  1. CartBadge и ProductList са братя: и двата са деца на App. Отначало кошницата живее в ProductList, а CartBadge има свой брояч.
  2. Клик на „Добави“ стига до ProductList и списъкът брои 1. CartBadge не вижда това състояние: брат не чете състоянието на брат си. Той остава 0.
  3. Поправката: кошницата отива в най-близкия общ родител на двамата, App. Броячът вече няма свое копие.
  4. От App данните текат надолу като props: count към CartBadge, items към ProductList и qty към всяка ProductCard. Двамата братя четат една кошница.
  5. Детето не притежава кошницата, затова не я сменя. Кликът изпраща събитие add с продукта. ProductList го предава нагоре, а App го слуша с @add и вика add().
  6. App сменя items и новите props текат надолу: броячът казва 2, картата казва 2. Никой не ги е обновявал на ръка.
  7. Правилото: състоянието е на едно място, толкова високо, колкото трябва, и не по-високо. Данните слизат като props, промените се качват като събития.

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

Пробвай

Качи кошницата

Кошницата е в ProductList, а CartBadge брои свое число. Премести кошницата в App: CartBadge получава броя като prop, ProductList получава items и съобщава за „Добави“ нагоре. Проверката добавя хляб, мляко и хляб и гледа картите и брояча.

Vue

Идеята

Детето съобщава, родителят решава

ProductCard не знае как е устроена кошницата. Тя знае само, че някой е натиснал „Добави“ на нейния продукт. Затова съобщава какво се е случило, а не какво да се промени: „добави този продукт“, а не „увеличи qty на ред 2“. App решава какво значи това: нов ред или още една бройка.

Така картата става по-проста и може да стои навсякъде: в каталога, в „Препоръчани“, в търсенето. Тя не притежава данните и не ги пипа.

Събитието е глагол без on: add, remove, change. Детето го обявява в defineEmits(['add']), изпраща го с emit('add', product), а родителят го слуша с @add. Вторият аргумент на emit е това, което получава функцията на родителя.

Пробвай

Попълни

Какво стои на празното място, за да стигне кликът до родителя?

<script setup>
defineProps(['product']);
const emit = defineEmits(['add']);
</script>

<template>
  <button @click="('add', product)">Добави</button>
</template>

Пробвай

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

Кликът на „Добави“ не прави нищо, а конзолата е празна. Кой ред е виновен?

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

Идеята

Когато дървото стане дълбоко

Колко нагоре? Толкова високо, колкото трябва, и не по-високо. Ако само ProductCard ползва нещо (дали е отворено описанието), то остава в нея. Качваш го, когато втори компонент трябва да го види.

В истинския магазин кошницата трябва на много места: броячът горе, списъкът, страницата на продукта, поръчката. Общият родител е високо, а пътят до тях минава през компоненти, които не ползват кошницата, само я подават надолу. Това се казва prop drilling (прокарване на props). При две нива е нормално. При пет нива всеки компонент по пътя трябва да знае за данни, които не са негови.

За това има отделни инструменти: provide/inject и Pinia. Те дават данните на далечни потомци без компонентите по пътя. Те са в урока за общото състояние от модула „Ефекти, данни и формуляри“. Правилото остава същото: данните имат един собственик, а промените минават през него.

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

Props са само за четене. props.qty = 3 във Vue не сменя нищо и пише в конзолата [Vue warn] Set operation on key "qty" failed: target is readonly. Детето, което иска промяна, съобщава нагоре.

v-model на компонент е същото. <QtyInput v-model="qty" /> е съкратен запис на :modelValue="qty" и @update:modelValue="(v) => qty = v". Данните пак слизат като prop и пак се качват като събитие.

Защо defineEmits. Обявените събития са документация: виждаш какво казва компонентът. И Vue ги сверява: ако картата изпрати emit('added'), а е обявила само add, конзолата казва Component emitted event "added" but it is neither declared in the emits option….

Едно копие не е повдигане. ref(props.items) в детето не копира масива: това е същият реактивен масив на родителя и добавеното се вижда. Но items.value = items.value.filter(…) сменя само кутията на детето. Махането не стига до родителя, и двете кошници се разминават.

Упражнение

Кошницата отстрани

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

Vue

Тест

Провери се

1. CartBadge е в горната част, ProductList е в главната. И двата са деца на App. Къде трябва да е кошницата?
2. ProductCard прави emit('add', product), а App пише <ProductList :items="items" /> без @add. Какво става?
3. Защо ProductCard не сменя кошницата сама, а съобщава нагоре?
4. Кошницата трябва на компонент пет нива под App. Трите компонента по пътя само я подават надолу. Как се казва това?

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