Props: данни отгоре
Накратко: Props са данните, които компонентът получава от родителя, както функцията получава параметри; детето не ги сменя, а children подава съдържание отвън.
Проблем
Три пъти хляб
Каталогът трябва да покаже три продукта: хляб, мляко и сирене. Картата на продукта е готова от миналия урок, затова App я слага три пъти.
Конзола
И трите карти са хляб. Продуктът е записан вътре в ProductCard, и всяка карта показва него. Другият път е BreadCard, MilkCard и CheeseCard: три копия на един и същ компонент, и всяка промяна на картата трябва да се направи три пъти. Липсва начин да кажеш на картата кой продукт да покаже.
Идеята
Props: данни отгоре
Компонентът е функция. Функцията получава данните си като параметри, а компонентът ги получава като атрибути от родителя: компонента, който го ползва. Тези атрибути се казват props (от properties). Родителят ги пише, детето ги чете.
<ProductCard product={bread} /><ProductCard product={milk} />export default function ProductCard({ product }) { return <h2>{product.name}</h2>;}React събира всички атрибути в един обект и го подава на функцията: { product: bread }. Записът ({ product }) взима свойството product от него (destructuring). Без скобите параметърът е целият обект: function ProductCard(props) и после props.product.name.
Картата е една, а всяко ползване е с други данни. Така е и с функцията: една дефиниция, много извиквания с различни аргументи.
Виж го
Виж как текат
const bread = { name: 'Бял хляб', price: 1.2 };const milk = { name: 'Прясно мляко', price: 1.6 };const cheese = { name: 'Сирене', price: 7.9 };
function ProductCard({ product, discount = 0 }) { return ( <article> <h2>{product.name}</h2> <p>Цена: {money(product.price * (1 - discount / 100))}</p> </article> );}
export default function App() { return ( <main> <h1>Хляб и мляко</h1> <div> <ProductCard product={bread} /> <ProductCard product={milk} /> <ProductCard product={cheese} discount={10} /> </div> </main> );}Стъпка по стъпка, с думи
- App държи данните: три продукта. Под него са три ProductCard: един и същи компонент три пъти, и никой още не знае какво да покаже.
- product={bread} подава обекта bread на първата карта. Атрибутите на компонента се казват props.
- Другите две карти получават своя продукт. Третата получава и discount = 10. Всяка карта има свои props.
- Всяка карта се рисува от своите props. Хлябът и млякото не са получили discount, затова е 0 по подразбиране. Сиренето е с 10% отстъпка: 7,11 €.
- Ако картата опита props.discount = 0, нагоре не тръгва нищо: в режима за разработка React хвърля TypeError (обектът с props е замразен), а данните в App остават същите.
- Данните текат само в една посока: от родителя надолу към детето. За да се смени нещо горе, детето трябва да помоли родителя. Това е модулът „Състояние“.
Всяка карта знае само своите props. Не вижда другите карти и не знае откъде идват данните в App.
Виж го
Една карта, три продукта
Конзола
Продуктите са в App, а ProductCard е един. Добави четвърти продукт в App (картинката img/apples.svg има) и още един <ProductCard … />. Картата не я пипаш. При 30 продукта ще искаш списък, а не 30 реда: той е следващият урок.
Пробвай
Намери грешката
Картата не се показва, а в конзолата пише „TypeError: Cannot read properties of undefined (reading 'toFixed')“.
Избери реда с грешката.
product="bread" подава текста „bread“, а не обекта. В текста няма price, затова product.price е undefined и money пада на toFixed. Обектът се подава с фигурни скоби: product={bread}.Идеята
Стойност по подразбиране
Някои продукти са с отстъпка. Картата получава втори prop, discount, в проценти. Повечето продукти нямат отстъпка и не е хубаво всеки ред в App да пише discount={0}. Ако prop липсва, той е undefined, както липсващ аргумент на функция. Цената става price * (1 - undefined / 100), тоест NaN.
Затова prop-ът получава стойност по подразбиране:
export default function ProductCard({ product, discount = 0 }) {Това е стойност по подразбиране при destructuring в JavaScript: ако discount липсва, е 0. В по-стар код ще видиш ProductCard.defaultProps = { discount: 0 } и propTypes. React 19 вече не ги поглежда за функционални компоненти: проверката на типовете е работа на TypeScript.
Пробвай
Отстъпка
App подава на сиренето discount 10 (процента). Направи картата да приема discount, 0 по подразбиране, и цената да е с отстъпката: хлябът 1,20 €, млякото 1,60 €, сиренето 7,11 €.
Конзола
Решение
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product, discount = 0 }) {
return (
<article>
<h2>{product.name}</h2>
<p>Цена: {money(product.price * (1 - discount / 100))}</p>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const bread = { name: 'Бял хляб', price: 1.2 };
const milk = { name: 'Прясно мляко', price: 1.6 };
const cheese = { name: 'Сирене', price: 7.9 };
export default function App() {
return (
<main>
<h1>Хляб и мляко</h1>
<div>
<ProductCard product={bread} />
<ProductCard product={milk} />
<ProductCard product={cheese} discount={10} />
</div>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
function ProductCard({ product, discount = 0 }) и цената money(product.price * (1 - discount / 100)). Без = 0 хлябът и млякото показват „NaN €“: проверката гледа и трите цени.
Идеята
Само за четене
Данните са на този, който ги държи. Картата може да чете своите props, но не и да ги сменя. Пробвай бутона „Още едно“: детето опитва да увеличи своя qty.
Конзола
Броят остава 2, а конзолата казва „TypeError: Cannot assign to read only property 'qty' of object“. В режима за разработка React замразява обекта с props, точно за да хване такъв код.
Дори да мине, промяната щеше да е само в детето: в App пак щеше да пише 2, и при следващото рисуване App щеше да подаде 2 наново. Две места с различна истина са бъгът от „Списък от данни“. Правилото е: данните текат надолу, а промяната е работа на родителя. Как детето моли родителя да смени нещо, е модулът „Състояние“.
Идеята
Съдържание отвън
Магазинът има рамки, които изглеждат еднакво, а вътре е различно: „Доставка“ с два абзаца, „Връщане“ с един. Заглавието е prop. Съдържанието е цял HTML и е по-удобно да се пише между отварящия и затварящия таг, както при <section>.
<Card title="Доставка"> <p>До 2 работни дни.</p></Card>export default function Card({ title, children }) { return ( <section className="card"> <h2>{title}</h2> {children} </section> );}Всичко между <Card> и </Card> идва в специалния prop children. Card решава къде да го сложи.
Конзола
Пробвай
Рамката
Card показва само заглавието. Направи го да показва и съдържанието, което App пише между <Card> и </Card>, под заглавието.
Конзола
Решение
Card.jsxexport default function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
App.jsximport Card from './Card';
export default function App() {
return (
<main>
<h1>Кошницата</h1>
<Card title="Доставка">
<p>До 2 работни дни.</p>
<p>Безплатна над 30 €.</p>
</Card>
<Card title="Връщане">
<p>До 14 дни от доставката.</p>
</Card>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
Card приема и children: function Card({ title, children }), и го слага под заглавието: {children}. Без фигурните скоби на страницата пише думата „children“. Ако го сложиш в <h2>, абзаците стават част от заглавието и то вече не е „Доставка“.
По-точно, за любопитните
Повече от едно място за съдържание. children е един. Ако рамката има и долна част, тя е още един prop с JSX: <Card title="Доставка" footer={<a href="/delivery">Подробно</a>}>…</Card>, а Card слага {footer} където трябва.
Всички свойства наведнъж. <ProductCard {...product} /> подава всяко свойство на обекта като отделен prop: name, price, image. Удобно е, но не се вижда какво точно получава картата.
Замразено е само първото ниво. props.qty = 3 хвърля грешка, но props.product.price = 2 минава тихо: обектът на продукта не е замразен, той е същият обект като в App. Така детето сменя данните на родителя зад гърба му. Не го прави.
Типовете. React 19 вече не проверява propTypes на функционалните компоненти и не ползва defaultProps за тях. Типовете на props се пишат с TypeScript: той е в урока за истинския проект.
Упражнение
Витрината
Направи витрината от празно. ProductCard получава product и discount (0 по подразбиране) и показва картинката (alt е името), името като заглавие от второ ниво и „Цена: …“ с отстъпката. Card получава title и показва заглавието (второ ниво) и съдържанието отвън. В App, под заглавието: трите карти в div (сиренето с discount 10), после Card „Доставка“ с „До 2 работни дни.“ и „Безплатна над 30 €.“.
Конзола
Решение
App.jsximport ProductCard from './ProductCard';
import Card from './Card';
const bread = { name: 'Бял хляб', price: 1.2, image: 'img/bread.svg' };
const milk = { name: 'Прясно мляко', price: 1.6, image: 'img/milk.svg' };
const cheese = { name: 'Сирене', price: 7.9, image: 'img/cheese.svg' };
export default function App() {
return (
<main>
<h1>Витрината</h1>
<div>
<ProductCard product={bread} />
<ProductCard product={milk} />
<ProductCard product={cheese} discount={10} />
</div>
<Card title="Доставка">
<p>До 2 работни дни.</p>
<p>Безплатна над 30 €.</p>
</Card>
</main>
);
}
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product, discount = 0 }) {
return (
<article>
<img src={product.image} alt={product.name} />
<h2>{product.name}</h2>
<p>Цена: {money(product.price * (1 - discount / 100))}</p>
</article>
);
}
Card.jsxexport default function Card({ title, children }) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
main > div { display: flex; flex-wrap: wrap; gap: 12px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 8px 12px; width: 140px; }
article img { width: 56px; height: 56px; }
h2 { font-size: 16px; margin: 4px 0; }
p { margin: 4px 0; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; background: #fbf8f2; padding: 4px 12px; margin-top: 12px; max-width: 320px; }
button { font: inherit; margin-left: 8px; }
ProductCard: function ProductCard({ product, discount = 0 }), <img src={product.image} alt={product.name} />, <h2>{product.name}</h2> и цената с отстъпката. Card: function Card({ title, children }) със <section className="card">, <h2>{title}</h2> и {children}. App: <ProductCard product={cheese} discount={10} /> и съдържанието между <Card title="Доставка"> и </Card>. Проверката търси картините по alt, цените на трите карти и абзаците вътре в .card.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Текст, връзки, картинки, таблици · Достъпност · Списък от данни · Първият компонент
- Продължава в: Списъци и ключове · Условия: празно, грешка, зарежда · Състояние · Изчислено състояние
- Задачи по темата: Компоненти