Условия: празно, грешка, зарежда
Накратко: Условното рендериране показва само частта от разметката, която отговаря на данните (зарежда, грешка, празно или списък); в React с && и тернарен оператор.
Проблем
Зарежда, грешка и списък едновременно
Каталогът е зареден. Под заглавието обаче пише „Зареждане…“, после „Продуктите не се заредиха.“, после „Няма продукти в тази категория.“ и чак тогава са продуктите.
Конзола
Компонентът Catalog получава status и продуктите като props. Той обаче не ги гледа: рисува всичките четири части винаги. Купувачът не знае на кое да вярва, а екранният четец прочита и грешката, и продуктите.
Идеята
Частта, която отговаря на данните
Каталогът има четири състояния: зарежда, грешка, празно и готов. На екрана трябва да е само частта за текущото. Това е условно показване: компонентът решава какво да върне според данните.
В JSX има три начина, според това колко неща се сменят:
// Цял друг изглед: if и ранен returnif (status === 'loading') return <p role="status">Зареждане…</p>;
// Едно от две: ? :<p>{product.stock === 0 ? 'Изчерпан' : 'В наличност'}</p>
// Нещо или нищо: &&{status === 'error' && <p role="alert">Продуктите не се заредиха.</p>}if е обикновеният JavaScript: компонентът е функция и може да върне различен JSX. ? : и && са изрази, затова могат да стоят вътре в JSX, в {}.
Двете библиотеки решават едно и също по свой начин: React с JavaScript около разметката, Vue с атрибути в нея. Резултатът е един и същ: елементът на невярното условие го няма в DOM-а. Не е скрит, а не съществува.
Тук status е даден като prop. Как каталогът го сменя сам, докато зарежда от API, е в модула „Ефекти, данни и формуляри“.
Виж го
Виж кой клон влиза
Една стойност влиза в компонента, а в DOM-а влиза само един клон. Гледай колона „DOM“ при всяка смяна на status.
function Catalog({ status, products }) { if (status === 'loading') return <p>Зареждане…</p>; if (status === 'error') return <p>Грешка</p>; if (status === 'empty') return <p>Няма продукти</p>; return <ul>…</ul>;}
// в DOM, но скрито<p hidden={status !== 'loading'}>Зареждане…</p>Стъпка по стъпка, с думи
- status идва в компонента като prop. Тя е 'loading': първото if съвпада и компонентът връща само <p>Зареждане…</p>. В DOM-а влиза само той.
- status става 'error'. Библиотеката рисува наново: сега съвпада вторият клон. <p>Зареждане…</p> излиза от DOM-а, а съобщението за грешка влиза.
- 'empty': само съобщението, че категорията е празна.
- 'ready': нито едно if не съвпада и компонентът стига до последния return: списъка. В DOM-а е винаги един клон.
- Другият начин: елементът се рисува винаги, а атрибутът hidden решава дали се вижда. При loading hidden е false и текстът се вижда.
- status става 'ready'. Елементът остава в DOM-а, с всичко в него, и само получава display: none. Не се вижда, а екранният четец го прескача.
- Условие с if, ? : или && маха елемента от DOM-а. hidden само го скрива. Махането е по подразбиране, а скриването е за нещо, което се сменя често или трябва да пази написаното.
Първата част е условно показване: при нова стойност старият елемент излиза от DOM-а, а новият влиза. Втората част е скриване: елементът е в DOM-а през цялото време и само се показва или скрива. За нея е краят на урока.
Пробвай
Четирите състояния
Catalog рисува всичко наведнъж. Направи го така, че във всеки раздел да е само частта на неговото състояние: „Зареждане…“ при loading, грешката при error, „Няма продукти в тази категория.“ при empty и списъкът при ready. Другите части да ги няма в страницата.
Конзола
Решение
Catalog.jsximport ProductCard from './ProductCard';
export default function Catalog({ status, products }) {
if (status === 'loading') return <p role="status">Зареждане…</p>;
if (status === 'error') return <p role="alert">Продуктите не се заредиха.</p>;
if (status === 'empty') return <p>Няма продукти в тази категория.</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
);
}
App.jsximport Catalog from './Catalog';
const PRODUCTS = [
{ id: 1, name: 'Бял хляб', price: 1.2, stock: 5 },
{ id: 3, name: 'Сирене', price: 7.9, stock: 0 },
{ id: 2, name: 'Прясно мляко', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<section aria-label="Зарежда">
<h2>Зарежда</h2>
<Catalog status="loading" products={[]} />
</section>
<section aria-label="Грешка">
<h2>Грешка</h2>
<Catalog status="error" products={[]} />
</section>
<section aria-label="Празна категория">
<h2>Празна категория</h2>
<Catalog status="empty" products={[]} />
</section>
<section aria-label="Готов">
<h2>Готов</h2>
<Catalog status="ready" products={PRODUCTS} />
</section>
</main>
);
}
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
<button type="button" aria-label={`Добави ${product.name}`}>Добави</button>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
if с ранен return за трите съобщения, а списъкът е последният return. Проверката търси във всеки раздел и съобщенията, и списъка. Ако списъкът е без условие, той е и в раздела „Зарежда“, макар и празен: <ul> без редове пак е списък за екранния четец.Пробвай
Попълни
„Зареждане…“ трябва да се вижда само докато status е loading.
{status === 'loading' <p role="status">Зареждане…</p>}&&: ако лявата страна е невярна, изразът е false и React не рисува нищо. Ако е вярна, изразът е елементът вдясно.
Идеята
Изчерпан: значка и изключен бутон
Условието не е само за цели части. В картата на продукта то сменя текст и атрибут:
{product.stock === 0 ? ( <p className="badge">Изчерпан</p>) : ( <p>Остават {product.stock} бр.</p>)}<button type="button" disabled={product.stock === 0}>Добави</button>disabled={true} слага атрибута disabled, а disabled={false} го маха.
Изключеният бутон не се натиска, не взима фокус с Tab, а екранният четец казва, че е недостъпен. Значката казва защо. Бутон, който се натиска, а после казва „няма наличност“, е по-лош: купувачът разбира едва след клика.
Пробвай
Изчерпан
Направи ProductCard така, че при stock 0 да показва „Изчерпан“ и бутонът „Добави“ да е изключен. Иначе да показва „Остават 5 бр.“ и бутонът да работи. Сиренето е изчерпано.
Конзола
Решение
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
{product.stock === 0 ? (
<p className="badge">Изчерпан</p>
) : (
<p>Остават {product.stock} бр.</p>
)}
<button type="button" aria-label={`Добави ${product.name}`} disabled={product.stock === 0}>Добави</button>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const PRODUCTS = [
{ id: 1, name: 'Бял хляб', price: 1.2, stock: 5 },
{ id: 3, name: 'Сирене', price: 7.9, stock: 0 },
{ id: 2, name: 'Прясно мляко', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<h1>Каталогът</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
? : избира между значката и „Остават … бр.“, а disabled={product.stock === 0} изключва бутона. Проверката търси и самотна 0 в страницата: {product.stock && <p>…</p>} при сиренето рисува 0. За това е следващата карта.Идеята
Нулата на екрана
&& не връща true или false. Той връща лявата страна, ако тя е невярна, и дясната, ако е вярна. React рисува числата, включително 0, и не рисува false, null и undefined. Затова:
{items.length && <p>В кошницата: {items.length}</p>} // празна кошница: 0{items.length > 0 && <p>В кошницата: {items.length}</p>} // празна кошница: нищоПред && слагай сравнение, което дава true или false: > 0, === 0, !== null. Друг път е ? : с null за „нищо“.
Пробвай
Намери грешката
При празна кошница в горния ъгъл пише „0“.
Избери реда с грешката.
Ред 4: items.length && … при празна кошница е 0, а React рисува 0. Трябва items.length > 0 && ….
Идеята
Махнато или скрито
Понякога елементът трябва да остане в DOM-а и само да не се вижда. Тук и двата панела имат поле за бележка. Отвори двата, напиши нещо, затвори ги и ги отвори пак.
Конзола
{removedOpen && …}: при затваряне го няма, а при отваряне е нов, с празно поле. Във втория е с атрибута hidden={!hiddenOpen}: той остава в DOM-а със display: none и пази написаното.Махането е по подразбиране: по-малко елементи в страницата, нищо скрито за екранния четец. Скриването е за части, които се отварят и затварят често или трябва да пазят какво е въведено в тях.
По-точно, за любопитните
hidden и CSS. hidden работи през display: none от стиловете на браузъра. Ако CSS правило даде на елемента display: flex, правилото печели и елементът пак се вижда. Тогава скриваш с клас или със style={{ display: 'none' }}.
Едно и също място, един и същ елемент. При {isSearch ? <input placeholder="Търси" /> : <input placeholder="Код" />} и двата клона са <input> на едно и също място. React запазва елемента и сменя само placeholder, а написаното остава. Ако трябват два различни елемента, дай им различни key.
return null. Компонент, който не трябва да рисува нищо, връща null.
Класовите компоненти в стария код правят същото в метода render().
aria-hidden не е скриване. aria-hidden="true" скрива елемента само от екранния четец, а на екрана той остава. За скриване от всички е display: none: hidden, v-show или махане.
Упражнение
Кошницата
App показва три кошници: празна, с продукти и с изчерпан продукт. Направи Cart така, че празната да казва „Кошницата е празна.“, без списък и сума, и бутонът „Поръчай“ да е изключен. Пълната показва редовете, „Общо: 4,00 €“ и работещ бутон. Изчерпаният продукт има значка „Изчерпан“, над бутона пише „Махни изчерпаните продукти, за да поръчаш.“ (role alert), а бутонът е изключен.
Конзола
Решение
Cart.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function Cart({ items }) {
const total = items.reduce((sum, p) => sum + p.price * p.qty, 0);
const soldOut = items.some((p) => p.stock === 0);
return (
<div>
{items.length === 0 ? (
<p>Кошницата е празна.</p>
) : (
<>
<ul>
{items.map((p) => (
<li key={p.id}>
{p.name} × {p.qty}
{p.stock === 0 && <span className="badge">Изчерпан</span>}
</li>
))}
</ul>
<p>Общо: {money(total)}</p>
</>
)}
{soldOut && <p role="alert">Махни изчерпаните продукти, за да поръчаш.</p>}
<button type="button" disabled={items.length === 0 || soldOut}>Поръчай</button>
</div>
);
}
App.jsximport Cart from './Cart';
export default function App() {
return (
<main>
<section aria-label="Празна">
<h2>Празна</h2>
<Cart items={[]} />
</section>
<section aria-label="С продукти">
<h2>С продукти</h2>
<Cart items={[
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Прясно мляко', price: 1.6, qty: 1, stock: 2 },
]} />
</section>
<section aria-label="С изчерпан продукт">
<h2>С изчерпан продукт</h2>
<Cart items={[
{ id: 1, name: 'Бял хляб', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'Сирене', price: 7.9, qty: 1, stock: 0 },
]} />
</section>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
items.length === 0 ? … : … избира между съобщението и списъка със сумата (двете са в <>…</>). soldOut && <p role="alert">…</p> показва предупреждението, а disabled={items.length === 0 || soldOut} изключва бутона в двата случая. С items.length && … празната кошница показва 0, а проверката го търси.Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Данни от API · Първият компонент · Props: данни отгоре · Списъци и ключове
- Продължава в: Обновяване на обекти и масиви · Изчислено състояние · Вдигане на състоянието · Ефекти
- Задачи по темата: Компоненти