Състояние
Накратко: Състоянието (state) е стойност, която компонентът помни между рисуванията; в React се пази с useState, а всяка промяна рисува компонента наново.
Проблем
Бутонът не брои
Картата на продукта има бутон „Добави“ и брояч „В кошницата“. Натисни „Добави“ три пъти.
Конзола
Конзолата казва count = 1, 2, 3. На картата пише 0. Грешка няма. Променливата count се сменя, а екранът не я следва. Къде е разликата?
Идеята
Обикновената променлива не стига
Картата не се рисува от твоя код, а от библиотеката. Тя рисува наново само когато знае, че нещо се е сменило. count++ сменя променливата, но не казва на никого.
ProductCard е функция. React я вика, когато рисува картата, и тя връща какво да се покаже. let count = 0 е локална променлива на това извикване. Кликът сменя нея, но никой не вика функцията наново, затова картата остава с 0.
Има и втори проблем. Ако React извика функцията наново по друга причина, първият ред е пак let count = 0. Всичко, натрупано в променливата, се губи.
Трябват данни, които компонентът помни между две рисувания и чиято промяна го рисува наново. Това се казва състояние (state).
Идеята
useState и ref
const [count, setCount] = useState(0);
function add() { setCount(count + 1);}useState(0)връща две неща: стойността и функцията, която я сменя. Нулата е само началната стойност.countе стойността за това рисуване.setCount(1)запомня новата стойност и казва на React да нарисува картата наново. React викаProductCardпак, аuseStateтози път връща 1.
useState е hook: функция на React, която се вика в началото на компонента, никога в if или цикъл. React разпознава състоянията по реда, в който са извикани.
Обработчикът на клика се дава като функция: onClick={add}. Ако напишеш onClick={add()}, add се вика още докато картата се рисува. Тя сменя състоянието, което рисува пак, и React спира с грешка „Too many re-renders“.
Конзола
Виж го
Виж какво става при клик
function ProductCard({ product }) { const [count, setCount] = useState(0);
function addThree() { setCount(count + 1); setCount(count + 1); setCount(count + 1); }
return ( <article> <button onClick={addThree}>+3</button> <p>В кошницата: {count}</p> </article> );}Стъпка по стъпка, с думи
- Картата пази count в състоянието на React: useState(0) дава стойността и функцията, която я сменя.
- React вика функцията ProductCard. Тя не пипа страницата, а връща описание: бутон и текст с count = 0.
- Първия път описанието става истински елементи в DOM-а. Страницата вече показва „В кошницата: 0“.
- Клик на +3: браузърът вика addThree.
- Три пъти setCount(count + 1). В това извикване count е снимка: 0. Всеки път на опашката отива „стани 1“. Екранът още не се сменя.
- Обработчикът свърши. React прилага опашката: 1, 1, 1 дава 1. После вика ProductCard наново и сега count е 1.
- React сравнява новото описание със старото. Бутонът е същият, сменен е само текстът. В DOM-а се сменя само той.
- Затова броячът показва 1, а не 3. Когато новата стойност зависи от старата, подай функция: setCount((c) => c + 1).
Картата не се строи наново. Библиотеката сравнява новото описание със старото и сменя в DOM-а само разликата: текста. Бутонът е същият елемент и фокусът остава на него. Това е разликата с render() от модула за DOM, която сменяше всичко.
Пробвай
Поправи брояча
Това е картата от началото. Направи count състояние, така че след всеки клик на „Добави“ картата да показва новия брой.
Конзола
Решение
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Добави ${product.name}`}>
Добави
</button>
<p>В кошницата: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 1, name: 'Бял хляб', price: 1.2 };
export default function App() {
return (
<main>
<h1>Магазинът</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 става const [count, setCount] = useState(0), а count++ става setCount(count + 1). useState трябва да се внесе: import { useState } from 'react'. Ако остане count++, стойността се сменя, но React не рисува наново.Идеята
Стойността е снимка
Минералната вода се продава и на стек от 6 бутилки. Удобно е бутонът за стек да вика add() шест пъти. Тук двете библиотеки се държат различно.
count е константа на едно рисуване. setCount не сменя count в обработчика, който тече в момента. То поставя заявка на опашка, а React я прилага след обработчика:
function addBox() { for (let i = 0; i < 6; i++) { setCount(count + 1); // 0 + 1, шест пъти: „стани 1“ }}Шестте заявки са „стани 1“ и броячът става 1, а не 6. Кажи „добави 1 към последната стойност“: подай функция вместо стойност.
setCount((c) => c + 1);React вика всяка такава функция с резултата от предишната: 0 → 1 → 2 … → 6. Правилото: когато новата стойност зависи от старата, подавай функция.
Пробвай
Намери грешката
„Добави стек“ трябва да добави 6 бутилки. Кой ред е виновен?
Избери реда с грешката.
Ред 5: count е 0 във всяко от шестте извиквания, затова заявките са „стани 1“. Трябва setCount((c) => c + 1) или направо setCount((c) => c + 6).
Пробвай
Стек вода
Направи addBox() така, че „Добави стек (6 бр.)“ да добавя 6 бутилки към това, което вече е в кошницата. Проверката натиска „Добави“ веднъж, после стека два пъти и очаква 1, 7 и 13.
Конзола
Решение
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(0);
function add() {
setCount((c) => c + 1);
}
function addBox() {
setCount((c) => c + 6);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<button type="button" onClick={add} aria-label={`Добави ${product.name}`}>
Добави
</button>
<button type="button" onClick={addBox}>Добави стек (6 бр.)</button>
<p>В кошницата: {count}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const product = { id: 15, name: 'Минерална вода', price: 0.9 };
export default function App() {
return (
<main>
<h1>Магазинът</h1>
<ProductCard product={product} />
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
setCount((c) => c + 6) добавя 6 към последната стойност. Шест пъти setCount(count + 1) дава 2: count е 1 във всяко от тях. setCount(6) не добавя, а слага 6, и след „Добави“ броячът е 6, а не 7. В решението и add е с функция: така двата бутона не зависят от снимката.Идеята
Всяка карта помни своето
Състоянието е на екземпляра, а не на кода. Каталогът има две карти от един и същ ProductCard, и всяка има свой count:
Конзола
Натисни „Добави“ на хляба два пъти и на млякото веднъж: 2 и 1. Библиотеката пази състоянието на всяка карта до мястото ѝ в дървото на компонентите. Махнеш ли картата от страницата, състоянието ѝ изчезва с нея. Когато две карти трябва да показват едно и също число, то трябва да е в общия им родител. За това е урокът за споделеното състояние в този модул.
По-точно, за любопитните
Защо снимка. React не сменя count на място, защото функцията е извикана с тази стойност и всичко в нея, и обработчиците, вижда нея. Така едно рисуване е последователно: каквото е пресметнато от count, е пресметнато от една и съща стойност.
Кога React не рисува. Ако setCount получи същата стойност (сравнена с Object.is), React пропуска рисуването. Това е важно в следващия урок, за масивите.
Обновяванията се събират. Всички setCount в един обработчик дават едно рисуване, а от React 18 и в setTimeout и след await.
StrictMode. В разработка <StrictMode> вика всеки компонент два пъти, за да хване компонент, който сменя нещо отвън, докато се рисува. Пясъчникът не го ползва, а Vite проектът го има в main.jsx.
Класови компоненти. По-стар React код пише компонента като клас със this.state и this.setState. Новият код е с функции и hooks.
Упражнение
Избери броя
Каталогът има три карти. Във всяка добави брой, който започва от 1, бутон „−“ (име за екранния четец: „По-малко Бял хляб“) и бутон „+“ („Повече Бял хляб“) и ред „2 × 1,20 € = 2,40 €“ с броя, цената и сумата. „−“ е забранен (disabled), когато броят е 1. Всяка карта брои сама.
Конзола
Решение
ProductCard.jsximport { useState } from 'react';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
const [count, setCount] = useState(1);
function less() {
setCount(count - 1);
}
function more() {
setCount(count + 1);
}
return (
<article className="card">
<h2>{product.name}</h2>
<p>{money(product.price)}</p>
<p>
<button type="button" onClick={less} disabled={count === 1} aria-label={`По-малко ${product.name}`}>−</button>
<button type="button" onClick={more} aria-label={`Повече ${product.name}`}>+</button>
</p>
<p>{count} × {money(product.price)} = {money(count * product.price)}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const products = [
{ id: 1, name: 'Бял хляб', price: 1.2 },
{ id: 5, name: 'Прясно мляко', price: 1.6 },
{ id: 7, name: 'Сирене', price: 7.9 },
];
export default function App() {
return (
<main>
<h1>Магазинът</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard: const [count, setCount] = useState(1), less вика setCount(count - 1), а more вика setCount(count + 1). „−“ е disabled={count === 1}: така броят не може да падне под 1. Сумата е money(count * product.price) направо в JSX: тя следва от count и не е отделно състояние. За това е урокът за изчисленото състояние.Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Събития · Първият компонент · Props: данни отгоре · Списъци и ключове
- Продължава в: Обновяване на обекти и масиви · Изчислено състояние · Вдигане на състоянието · Ефекти
- Задачи по темата: Състояние