Първият компонент
Накратко: React компонентът е функция, която връща JSX: описва как изглежда екранът според данните, а React сменя в страницата само разликата.
Проблем
Полето губи фокуса
Модулът „JavaScript в страницата“ свърши с кошница, която се рисува от данните с една render(). Тук всяка промяна на полето за брой вика render(). Купувачът иска 15 млека: щраква в полето, трие 2 и пише 15. Опитай го.
Конзола
След триенето полето е празно и вече не е на фокус: 1 и 5 не стигат до никъде. render() е сменила полето с ново още при първия клавиш. Новото поле изглежда същото, но е друг елемент, а фокусът беше у стария. Ръчното пипане на отделни места забравя места. Рисуването на всичко наново губи фокуса. Има трети път.
Идеята
Интерфейсът като функция на данните
render() имаше добра идея: екранът се строи само от данните. При едни и същи данни екранът е един и същ, каквото и да е имало на него преди. Грешката беше в последната стъпка: render() сама маха и прави елементите.
Затова работата се дели на две:
- Ти пишеш функция, която казва как изглежда екранът при тези данни. Тя не пипа страницата, а връща описание: тук има
<main>, в него<h1>с текст „Кошницата“, после поле със стойност 2. - Библиотеката държи истинския DOM. Първия път го строи от описанието. При всяка промяна на данните взима новото описание, сравнява го със старото и сменя в страницата само разликата.
Такава функция се казва компонент. Накратко: екранът е функция на данните, а компонентът е тази функция. React и Vue правят това по различен начин. Идеята е една и съща, а изборът между тях е въпрос на екип, не на „по-добра“ библиотека.
Виж го
Първият компонент
В React компонентът е функция, която връща JSX: запис, който прилича на HTML, но е в JavaScript.
const product = { name: 'Прясно мляко', price: 1.6, stock: 35 };
export default function App() { return ( <main> <h1>{product.name}</h1> <p>Цена: {money(product.price)}</p> </main> );}function App()е обикновена функция. Името е с главна буква: така React различава компонент от HTML таг.return ( … )връща описанието. Скобите са само за да може JSX да започне на нов ред.{product.name}е израз на JavaScript във фигурни скоби. Резултатът му става текст в страницата.export defaultпозволява друг файл да внесе компонента. Пясъчникът сам слагаAppв страницата. В истински проект това прави един ред вmain.jsx:createRoot(...).render(<App />).
Конзола
Смени цената или наличността в обекта product и натисни „Изпълни“. Описанието на екрана не го пипаш: екранът следва данните.
Идеята
Правилата на JSX
JSX прилича на HTML, но не е HTML: преди да тръгне, всеки таг става извикване на функция в JavaScript. Затова има свои правила.
- Един корен.
returnвръща един елемент. Ако трябват няколко един до друг, обвиваш ги във фрагмент<>…</>: той не прави елемент в страницата. classNameвместоclass,htmlForвместоfor.classиforса думи на JavaScript. Сclassстраницата пак работи, но React пише грешка в конзолата: „Invalid DOM propertyclass. Did you meanclassName?“.{израз}в текста и в атрибутите:<img src={product.image} alt={product.name} />. Без кавички около скобите:alt="{product.name}"е текстът{product.name}, буква по буква.- Всеки таг е затворен, и тези без съдържание:
<input />,<img />,<br />. - Събитията са с camelCase и получават функция:
onClick={add}. НеonClick={add()}: това викаaddведнага, докато компонентът се рисува, и подава наonClickрезултата ѝ.
Пробвай
Намери грешката
Клик на „Добави“ не пише нищо в конзолата.
Избери реда с грешката.
onclick="add()" е запис от HTML. React приема събития само като onClick с функция: onClick={add}. Този атрибут React пропуска и пише грешка в конзолата: „Invalid event handler property onclick. Did you mean onClick?“.Пробвай
Попълни
Екранният четец трябва да прочете името на продукта като описание на картинката.
<img src={product.image} alt= />alt={product.name}: изразът е във фигурни скоби, без кавички.Идеята
Компоненти от компоненти
Страницата на магазина има части: горната част с менюто, картата на продукта, кошницата. Всяка част става компонент в свой файл, а по-големият компонент ги ползва като тагове. Така App не е една дълга функция, а няколко реда, които казват какво има на страницата.
export default function ShopHeader() { return ( <header> <a href="/">Магазинът</a> <nav>…</nav> </header> );}import ShopHeader from './ShopHeader';import ProductCard from './ProductCard';
export default function App() { return ( <> <ShopHeader /> <main> <ProductCard /> </main> </> );}Името на компонента започва с главна буква, и това не е въпрос на вкус. За React <ShopHeader /> е компонент, а <shopHeader /> е HTML таг <shopheader>, какъвто браузърът не познава. Тогава в конзолата има грешка, а горната част я няма. Фрагментът <>…</> е тук, защото ShopHeader и <main> са един до друг.
Пробвай
Сглоби страницата
ShopHeader и ProductCard са готови в своите файлове. Сглоби App: горе ShopHeader, а под заглавието „Млечни“ в main картата на продукта.
Конзола
Решение
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Млечни</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">Магазинът</a>
<nav>
<a href="/bread">Хляб</a>
<a href="/milk">Мляко</a>
<a href="/cart">Кошница</a>
</nav>
</header>
);
}
ProductCard.jsxconst product = { name: 'Прясно мляко', price: 1.6, stock: 35 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard() {
return (
<article>
<h2>{product.name}</h2>
<p>Цена: {money(product.price)}</p>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
App внася двата компонента с import и ги ползва като тагове: <ShopHeader /> преди <main>, <ProductCard /> в <main>. Двата са един до друг, затова са във фрагмент <>…</>. Проверката търси горната част по роля: <header> вътре в <main> вече не е горната част на страницата, а с малка буква <shopHeader /> няма никакъв <header>.
Идеята
Само разликата
Ето кошницата от проблема като компонент.
useState('2') е паметта на компонента: стойност, която React пази между две рисувания, и функция (setMilk), която я сменя и казва на React да нарисува компонента наново. Тя е темата на модула „Състояние“. Тук е само за да има какво да се сменя.
Конзола
Направи същото като в проблема: щракни в полето за мляко, изтрий 2 и напиши 15. Полето пази фокуса и написаното, а сумата става 25,20 €. Компонентът описва целия екран при всеки клавиш, както render(). Разликата е, че страницата не се строи наново. Библиотеката вижда, че в новото описание полето е на същото място, със същия таг, и сменя само неговата стойност и текста на сумата.
Виж го
Виж сравнението
import { useState } from 'react';const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function App() { const [milk, setMilk] = useState('2'); const [bread, setBread] = useState('1'); const total = Number(milk) * 1.6 + Number(bread) * 1.2;
return ( <main> <h1>Кошницата</h1> <label> Прясно мляко <input type="number" value={milk} onChange={(e) => setMilk(e.target.value)} /> </label> <label> Бял хляб <input type="number" value={bread} onChange={(e) => setBread(e.target.value)} /> </label> <p>Общо: {money(total)}</p> </main> );}Стъпка по стъпка, с думи
- Купувачът е изтрил 2 и е написал 1: данните казват milk = "1". По средата е последното описание, което компонентът е върнал, а вдясно е DOM-ът с фокуса в полето за мляко.
- Купувачът пише 5. onChange вика setMilk("15"): данните се сменят и React отбелязва, че App трябва да се нарисува наново.
- React вика функцията App наново. Тя не пипа страницата, а връща ново описание: дърво от обикновени обекти. То се строи бързо.
- Библиотеката сравнява новото описание с предишното. Същите тагове в същия ред и две разлики: стойността на полето и текстът на сумата.
- В DOM-а влизат само двете разлики. <input> е същият елемент като преди, затова фокусът и курсорът остават в него.
- Ти описваш целия екран за дадените данни, както с render(). Библиотеката намира какво се е сменило и пипа само него. Двете библиотеки правят това, всяка по своя начин.
Описанието е евтино: то е дърво от обикновени обекти и компонентът го прави наново всеки път. Скъпото е DOM-ът, затова библиотеката пипа в него само разликите. Така ти пишеш като с render(): целия екран от данните. Цената на render() (изгубен фокус, хиляди нови елементи) не се плаща.
По-точно, за любопитните
Виртуален DOM. Описанието често се казва виртуален DOM (virtual DOM), а сравнението му със старото се казва reconciliation (React) или patch (Vue). Възел от описанието е обект като { type: 'h1', props: { children: 'Кошницата' } }.
Двата начина. JSX се компилира до извиквания като jsx('h1', { children: product.name }). Когато паметта на компонент се смени, React вика наново неговата функция и функциите на компонентите в него, после сравнява. И в двата случая накрая има ново описание, сравнение и малко промени в DOM-а.
Текстът е текст. {product.name} слага текст, както textContent. Име като <img src=x onerror=alert(1)> се вижда като знаци, а не става таг. HTML от низ се слага само с dangerouslySetInnerHTML, и името казва да внимаваш.
Класови компоненти. В по-стар код компонентът е клас: class Cart extends React.Component { render() { … } }. Работи и днес, но новият код е с функции и hooks (useState е hook).
StrictMode. Нов проект с Vite обвива App в <StrictMode>. В режима за разработка той вика всеки компонент два пъти, за да изкара наяве код, който пипа нещо извън себе си. Затова console.log в тялото на компонента може да се появи два пъти. Пясъчникът рисува App без StrictMode.
Упражнение
Картата на продукта
В ProductCard е обектът product (Сирене). Направи картата: името като заглавие от второ ниво, „Цена: 7,90 €“ (с money), „В наличност: 12 бр.“ и бутон „Добави“ с име за екранния четец „Добави Сирене“. Клик на бутона пише в конзолата „Добавен: Сирене“ (функцията add е готова). После сглоби App: ShopHeader горе, а под заглавието „Млечни“ в main картата.
Конзола
Решение
ProductCard.jsxconst product = { name: 'Сирене', price: 7.9, stock: 12 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard() {
function add() {
console.log(`Добавен: ${product.name}`);
}
return (
<article>
<h2>{product.name}</h2>
<p>Цена: {money(product.price)}</p>
<p>В наличност: {product.stock} бр.</p>
<button type="button" aria-label={`Добави ${product.name}`} onClick={add}>
Добави
</button>
</article>
);
}
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Млечни</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">Магазинът</a>
<nav>
<a href="/bread">Хляб</a>
<a href="/milk">Мляко</a>
<a href="/cart">Кошница</a>
</nav>
</header>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
Всичко от данните е в {…}: {product.name}, {money(product.price)}, {product.stock}. Името за екранния четец е израз: aria-label={`Добави ${product.name}`}. Бутонът получава функцията: onClick={add}. С onClick={add()} „Добавен: Сирене“ се появява в конзолата още при зареждане, без клик, а проверката гледа точно това.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Достъпност · DOM: страницата от JavaScript · Събития · Списък от данни
- Продължава в: Props: данни отгоре · Списъци и ключове · Условия: празно, грешка, зарежда · Състояние
- Задачи по темата: Компоненти