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

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

Накратко: Общото състояние в React се дава от доставчик горе в дървото с Context и useReducer, а компонентите го четат направо, без props през всички нива.

Проблем

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

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

React

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

Идеята

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

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

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

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

В React това е Context:

cart.js
export const CartContext = createContext(null);
// App.jsx
<CartContext value={{ items, count, add }}>
<Header />
<main><Catalog /></main>
</CartContext>
// CartBadge.jsx
const { count } = useContext(CartContext);

createContext(null) прави „канала“. null е стойността, ако няма доставчик над компонента. <CartContext value={…}> е доставчикът. useContext(CartContext) чете стойността на най-близкия доставчик нагоре по дървото. Когато стойността се смени, всеки компонент, който я чете, се рисува наново.

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

Виж го

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

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

const CartContext = createContext(null);
function App() {
const [items, setItems] = useState([]);
const count = items.reduce((s, i) => s + i.qty, 0);
function add(product) { /* … */ }
return (
<CartContext value={{ items, count, add }}>
<Header />
<Catalog />
</CartContext>
);
}
function Header() {
return <header><CartBadge /></header>;
}
function CartBadge() {
const { count } = useContext(CartContext);
return <a href="/cart">Кошница ({count})</a>;
}
function ProductCard({ product }) {
const { add } = useContext(CartContext);
return <button onClick={() => add(product)}>Добави</button>;
}
Стъпка по стъпка, с думи
  1. Кошницата живее в App. Трябва на два компонента далеч надолу: CartBadge в горната част и ProductCard в каталога.
  2. Само с props кошницата слиза ниво по ниво. Header, Catalog и ProductList не я ползват. Те само я подават.
  3. Някой пренаписва Header и забравя count. Веригата се къса по средата и броячът показва 0. Това е прокарването на props.
  4. Вместо това: App обвива дървото в <CartContext value={…}>. Всичко под него може да чете кошницата.
  5. CartBadge и ProductCard я четат с useContext(CartContext). Компонентите по средата не знаят нищо за кошницата.
  6. Клик на „Добави“ вика add(product). Функцията е дошла от доставчика, затова промяната става в App.
  7. Кошницата в App се сменя и всеки компонент, който я чете, се рисува наново: броячът казва 1, картата казва 1.
  8. Данните пак имат един собственик, App. Сменен е само пътят: не през всяко ниво, а направо от доставчика.

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

Пробвай

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

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

React

Идеята

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

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

React има за това вградената кука useReducer. Вместо setItems(…) компонентите изпращат действие (dispatch({ type: 'add', product })), а една функция, reducer, решава какво става с кошницата:

function cartReducer(items, action) {
switch (action.type) {
case 'add': /* нов масив с продукта */
default:
return items;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const add = (product) => dispatch({ type: 'add', product });
return <CartContext value={{ items, count, add }}>{children}</CartContext>;
}

CartProvider е доставчикът, а App само го слага около дървото. Потребителите не се сменят: те пак четат useContext(CartContext). Reducer-ът е обикновена функция: стара кошница и действие влизат, нова кошница излиза. Тя не пипа нищо друго и затова лесно се тества.

Context и useReducer са вградени в React. Има и библиотеки за хранилища, например Zustand и Redux Toolkit. Те ги няма в пясъчника и тук само ги споменаваме: идеята е същата, един източник и действия, които го сменят.

React
Отвори cart.jsx: там е цялата логика на кошницата. CartBadge и ProductCard са същите като в задачата.

Пробвай

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

Страницата пада, щом се отвори: TypeError: Cannot destructure property 'count' of … as it is null. Кой ред е виновен?

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

Идеята

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

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

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

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

Пробвай

Попълни

Как CartBadge чете брояча от доставчика?
import { useContext } from 'react';
import { CartContext } from './cart.js';

export default function CartBadge() {
  const { count } = (CartContext);
  return <a href="/cart">Кошница ({count})</a>;
}
По-точно, за любопитните

<CartContext.Provider>. До React 19 доставчикът се пишеше <CartContext.Provider value={…}>. Този запис още работи, а <CartContext value={…}> е новият, по-къс.

Собствена кука. Често до контекста има малка функция export function useCart() { return useContext(CartContext); }. Тя може и да хвърли ясна грешка, ако няма доставчик, вместо TypeError за null.

Кой се рисува наново. Всеки компонент, който чете контекста, се рисува наново, когато стойността му се смени. value={{ items, count, add }} е нов обект при всяко рисуване на доставчика. В голямо приложение това се пази с useMemo или с няколко по-малки контекста. В урока за бързината ще видиш как се мери.

Класовите компоненти четат контекст със static contextType или с <CartContext.Consumer>. Срещат се в стар код.

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

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

Упражнение

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

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

React

Тест

Провери се

1. Кошницата слиза от App през Catalog и ProductList до ProductCard. Catalog и ProductList не я ползват. Какво е лошото?
2. CartBadge вика useContext(CartContext), но е извън <CartContext value={…}>. Какво получава?
3. Защо cartReducer връща нов масив, а не прави items.push(…)?
4. Кое от тези трябва да е в общото състояние?
5. Списъкът с продукти идва от /api/products. Да го сложим ли в общото хранилище?

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