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

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

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

Проблем

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

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

React

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

Идеята

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

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

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

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

onAdd={add} подава функцията надолу. Детето я вика, когато нещо се случи: onClick={() => onAdd(product)}. Такава функция се казва callback.

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

Виж го

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

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

function App() {
const [items, setItems] = useState([]);
const count = items.reduce((s, i) => s + i.qty, 0);
function add(product) {
setItems(plusOne(items, product));
}
return (
<>
<CartBadge count={count} />
<ProductList items={items} onAdd={add} />
</>
);
}
function ProductList({ items, onAdd }) {
return PRODUCTS.map((p) => (
<ProductCard key={p.id} product={p}
qty={qtyOf(items, p)} onAdd={onAdd} />
));
}
function ProductCard({ product, qty, onAdd }) {
return <button onClick={() => onAdd(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. Детето не притежава кошницата, затова не я сменя. Кликът вика onAdd(product): функция, която App е подал надолу. Извикването стига до add() в App.
  6. App сменя items и новите props текат надолу: броячът казва 2, картата казва 2. Никой не ги е обновявал на ръка.
  7. Правилото: състоянието е на едно място, толкова високо, колкото трябва, и не по-високо. Данните слизат като props, промените се качват като събития.

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

Пробвай

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

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

React

Идеята

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

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

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

Имената следват една конвенция: prop с функция започва с on и казва събитието: onAdd, onRemove, onChange. Функцията в родителя често е add или handleAdd. Подаваш функцията, а не резултата ѝ: onClick={() => onAdd(product)}, не onClick={onAdd(product)}.

Пробвай

Попълни

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

export default function ProductCard({ product, onAdd }) {
  return <button onClick={() => (product)}>Добави</button>;
}

Пробвай

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

Страницата се отваря и броячът на кошницата расте сам, без клик. Кой ред е виновен?

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

Идеята

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

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

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

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

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

Props са само за четене. В режим на разработка React замразява обекта с props: props.qty = 3 хвърля TypeError: Cannot assign to read only property 'qty'. Детето, което иска промяна, съобщава нагоре.

Защо стрелка. onClick иска функция, която React да извика по-късно. onClick={add} също е функция, но я вика със събитието на клика, а не с продукта. Затова onClick={() => onAdd(product)}: нова функция, която знае продукта.

Класовите компоненти имат същия модел: родителят подава onAdd като prop и детето вика this.props.onAdd(product).

Едно копие не е повдигане. Ако родителят държи кошницата, а детето прави свое копие с useState(items), копието взима само първата стойност и спира да следва родителя. Това е грешката от урока за изчисленото състояние.

Упражнение

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

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

React

Тест

Провери се

1. CartBadge е в горната част, ProductList е в главната. И двата са деца на App. Къде трябва да е кошницата?
2. В ProductCard пише onClick={onAdd(product)}. Какво става?
3. Защо ProductCard не сменя кошницата сама, а съобщава нагоре?
4. Кошницата трябва на компонент пет нива под App. Трите компонента по пътя само я подават надолу. Как се казва това?

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