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

Първият компонент

Накратко: React компонентът е функция, която връща JSX: описва как изглежда екранът според данните, а React сменя в страницата само разликата.

Проблем

Полето губи фокуса

Модулът „JavaScript в страницата“ свърши с кошница, която се рисува от данните с една render(). Тук всяка промяна на полето за брой вика render(). Купувачът иска 15 млека: щраква в полето, трие 2 и пише 15. Опитай го.

HTML, CSS, JS

След триенето полето е празно и вече не е на фокус: 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 />).
React

Смени цената или наличността в обекта product и натисни „Изпълни“. Описанието на екрана не го пипаш: екранът следва данните.

Идеята

Правилата на JSX

JSX прилича на HTML, но не е HTML: преди да тръгне, всеки таг става извикване на функция в JavaScript. Затова има свои правила.

  • Един корен. return връща един елемент. Ако трябват няколко един до друг, обвиваш ги във фрагмент <>…</>: той не прави елемент в страницата.
  • className вместо class, htmlFor вместо for. class и for са думи на JavaScript. С class страницата пак работи, но React пише грешка в конзолата: „Invalid DOM property class. Did you mean className?“.
  • {израз} в текста и в атрибутите: <img src={product.image} alt={product.name} />. Без кавички около скобите: alt="{product.name}" е текстът {product.name}, буква по буква.
  • Всеки таг е затворен, и тези без съдържание: <input />, <img />, <br />.
  • Събитията са с camelCase и получават функция: onClick={add}. Не onClick={add()}: това вика add веднага, докато компонентът се рисува, и подава на onClick резултата ѝ.

Пробвай

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

Клик на „Добави“ не пише нищо в конзолата.

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

Пробвай

Попълни

Екранният четец трябва да прочете името на продукта като описание на картинката.

<img src={product.image} alt= />

Идеята

Компоненти от компоненти

Страницата на магазина има части: горната част с менюто, картата на продукта, кошницата. Всяка част става компонент в свой файл, а по-големият компонент ги ползва като тагове. Така App не е една дълга функция, а няколко реда, които казват какво има на страницата.

ShopHeader.jsx
export default function ShopHeader() {
return (
<header>
<a href="/">Магазинът</a>
<nav>…</nav>
</header>
);
}
App.jsx
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 картата на продукта.

React

Идеята

Само разликата

Ето кошницата от проблема като компонент.

useState('2') е паметта на компонента: стойност, която React пази между две рисувания, и функция (setMilk), която я сменя и казва на React да нарисува компонента наново. Тя е темата на модула „Състояние“. Тук е само за да има какво да се сменя.

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>
);
}
Стъпка по стъпка, с думи
  1. Купувачът е изтрил 2 и е написал 1: данните казват milk = "1". По средата е последното описание, което компонентът е върнал, а вдясно е DOM-ът с фокуса в полето за мляко.
  2. Купувачът пише 5. onChange вика setMilk("15"): данните се сменят и React отбелязва, че App трябва да се нарисува наново.
  3. React вика функцията App наново. Тя не пипа страницата, а връща ново описание: дърво от обикновени обекти. То се строи бързо.
  4. Библиотеката сравнява новото описание с предишното. Същите тагове в същия ред и две разлики: стойността на полето и текстът на сумата.
  5. В DOM-а влизат само двете разлики. <input> е същият елемент като преди, затова фокусът и курсорът остават в него.
  6. Ти описваш целия екран за дадените данни, както с 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 картата.

React

Тест

Провери се

1. Компонентът се рисува втори път със същите данни. Какво показва?
2. Кой ред е верен JSX?
3. Защо в компонента фокусът остава в полето, а с render() от предишния модул се губи?
4. Във файла има function productCard() и App ползва <productCard />. Какво става?

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