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

Изчислено състояние

Накратко: Изчисленото състояние е стойност, която се смята от състоянието при рисуване (брой, сума, филтриран списък), вместо да се пази отделно; за скъпа сметка useMemo пази резултата между рисуванията.

Проблем

Сумата изостава

Кошницата има бял хляб и прясно мляко. Натисни „+“ на хляба: броят и сумата растат. Сега махни млякото.

React

Редът изчезва, броят пада на 3, а сумата остава 5,20 €. Компонентът пази три състояния: items, count и total. Всеки клик трябва да смени и трите, а функцията за махане сменя две. Това е същото разминаване като в урока „Списък от данни“, само че вътре в компонент. Библиотеката рисува вярно това, което ѝ дадеш. Даваш ѝ две истини за една кошница.

Идеята

Изчисли, не пази

Попитай за всяко състояние: може ли да го изчисля от другото състояние или от props? Ако може, то не е състояние. Броят е сборът от количествата в items. Сумата е сборът от цена по количество. И двете се изчисляват.

Такава стойност се казва изчислено състояние (derived state). Пазиш само items, а останалото смяташ от тях всеки път, когато компонентът се рисува.

const [items, setItems] = useState(START);
const count = items.reduce((sum, i) => sum + i.qty, 0);
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);

setItems кара React да пусне функцията App наново. Тогава count и total се смятат от новите items. Това са обикновени константи. Не им трябва useState, не им трябва и нищо специално.

Сега функциите more и remove сменят само items. Няма какво да забравят.

Виж го

Виж изчисленото и копието

Вляво е кошницата и копие на сумата, пазено отделно. Вдясно са трите изчислени стойности.

const [items, setItems] = useState(START);
const [savedTotal, setSavedTotal] = useState(26.9);
const count = items.reduce((s, i) => s + i.qty, 0);
const total = items.reduce((s, i) => s + i.price * i.qty, 0);
const free = total >= 30;
function more(item) {
setItems(items.map((i) =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i,
));
setSavedTotal(savedTotal + item.price);
}
function remove(item) {
setItems(items.filter((i) => i.id !== item.id));
}
Стъпка по стъпка, с думи
  1. Вляво е състоянието: кошницата items. Под нея има второ състояние, savedTotal: същата сума, пазена отделно.
  2. Вдясно са count, total и free: обикновени константи. React пуска App при всяко рисуване и те се смятат наново от items всеки път: 26,90 €, остават 3,10 € до безплатна доставка.
  3. more(сирене): items се сменя и трите стрелки светват. Сумата е 34,80 €, доставката е безплатна. more() обновява на ръка и savedTotal, затова копието още е вярно.
  4. remove(сирене): items се сменя, а count, total и free следват сами: 3,20 €. Ред за savedTotal в remove() никой не е написал.
  5. Копието казва 34,80 €, а кошницата е 3,20 €. Грешка няма, има невярно число: състоянието и копието му са се разминали.
  6. Правилото: пази само това, което не може да се изчисли. Всичко, което следва от състоянието, се смята от него, и копие за забравяне няма.

Изчислените стойности следват кошницата при всяка промяна, защото нямат собствена памет. Копието следва само докато някой се сеща да го обнови. Първата функция, която забрави, го прави невярно.

Пробвай

Изчисли броя и сумата

Кошницата пази count и total като отделни състояния и remove() забравя сумата. Махни двете състояния и изчисли броя и сумата от items. Проверката натиска „+“ на хляба, после маха млякото и гледа броя и сумата.

React

Пробвай

Попълни

Броят се изчислява от кошницата. Какво стои на празното място?

const [items, setItems] = useState(START);
 count = items.reduce((sum, i) => sum + i.qty, 0);

Идеята

Кога да пазиш резултата

Константата се смята при всяко рисуване на компонента, дори когато items са същите и рисуването е заради друго състояние. За сума от десет реда това е нищо. Ако сметката е скъпа (подреждане на хиляди продукти), React има useMemo:

const sorted = useMemo(() => products.toSorted(byPrice), [products]);

React пази резултата и пуска функцията наново само ако някоя от зависимостите в масива ([products]) е различна от предишното рисуване. Ако забравиш зависимост, резултатът остава стар. Затова useMemo не е за всяка сума. Започни с константа и го добави, когато измериш, че сметката е бавна. Резултатът е същият, само по-рядко се смята.

Идеята

Филтърът също е изчислен

Каталогът има бутони „Всички“, „Хляб“ и „Мляко“. Изкушението е да пазиш два масива: всички продукти и видимите. Но видимите следват от два други факта: пълния списък и избраната категория. Пазиш само категорията:

const [category, setCategory] = useState('all');
const visible = category === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category === category);

Същото е с подреждането: пазиш „по цена“ или „по име“, а подреденият списък го изчисляваш. Внимавай с sort(): той подрежда масива на място, тоест сменя състоянието. toSorted() връща нов масив и оставя стария.

Пробвай

Само категорията

Бутоните сменят категорията, но списъкът винаги показва всичко. Изчисли видимите продукти от категорията и покажи „Показани: 3 от 5“ над списъка. Проверката избира „Мляко“, после „Хляб“, после „Всички“.

React

Идеята

Копие на prop

Същата грешка има и между компоненти. App дава намаление 20% и подава новите цени на всяка ProductCard. Картата обаче е запомнила цената, която е получила първия път:

React

Натисни „Намаление 20%“. Бутонът е натиснат, а хлябът още е 1,20 €.

const [price] = useState(product.price) взима product.price само при първото рисуване на картата. После React рисува картата с новия product, но състоянието ѝ е нейно и остава.

Поправката е да не копираш: показвай product.price направо. Ако картата трябва да покаже нещо, изчислено от prop (цена с ДДС, цена на брой), изчисли го при рисуване.

Пробвай

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

Купувачът натиска „+“: количеството на реда става 3, а сумата на реда остава като за 2. Кой ред е виновен?

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

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

Кога копието е нарочно. Понякога искаш стойност отвън само за начало: формуляр за редакция започва с текущото име, а после човекът пише. Тогава копието е правилно, но го кажи в името: prop initialName, състояние name. Който чете кода, вижда, че промените отвън не се следят.

useMemo е подсказка. React си пази правото да изхвърли запомнения резултат и да смята наново, и документацията го казва изрично. Затова кодът трябва да е верен и без useMemo. React Compiler, ако проектът го ползва, сам запомня такива сметки, и ръчният useMemo става още по-рядко нужен.

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

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

Упражнение

Безплатна доставка

Кошницата има бутони „−“ и „+“ за всеки ред (при „−“ на последната бройка редът изчезва). Под списъка покажи „Продукти: 4“, „Общо: 18,20 €“ и „До безплатна доставка: още 11,80 €“, а над 30 € (FREE_FROM) само „Доставката е безплатна“. Всичко се изчислява от items. Проверката добавя сирене до 34,00 €, после маха до под 30 € и маха хляба.

React

Тест

Провери се

1. Кошницата пази items. Кое от тези трябва да е отделно състояние?
2. Кога да увиеш сметка в useMemo?
3. const [price] = useState(product.price); в ProductCard. Родителят подава нова цена. Какво показва картата?
4. Списъкът се филтрира по категория. Какво да е в състоянието?

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