React: декларативный UI, JSX и первый компонент
В уроке 02 ты собрал чат на голом DOM: руками создавал <li>, руками
вставлял его в список, руками очищал input и руками скроллил вниз.
Каждое изменение состояния ты переносил в DOM сам. Это работает, но с
ростом интерфейса таких ручных синхронизаций становится сотни, и каждая
это место для бага.
React переворачивает задачу. Ты не описываешь шаги “найди узел, поменяй текст”. Ты описываешь результат: “при таком состоянии экран выглядит вот так”. Дальше runtime сам считает разницу и патчит DOM. Этот урок про то, как устроен этот переход и из чего собран первый React-компонент.
1. Императив против декларатива
Представь фотографа. В императивном подходе ты сам двигаешь модель по сцене: шаг влево, поворот, подними руку. В декларативном ты описываешь нужный кадр, а фотограф рисует его заново. Если надо поменять позу, ты меняешь описание, а не двигаешь модель вручную.
Вспомни submit из vanilla-чата:
form.addEventListener('submit', (event) => {
event.preventDefault();
const text = input.value.trim();
if (text === '') return;
append(text); // вручную создаём li и вставляем
save(/* ... */); // вручную синхронизируем localStorage
input.value = ''; // вручную чистим поле
input.focus(); // вручную возвращаем фокус
});
Четыре ручных шага, и ты обязан не забыть ни один. На React ты меняешь одну переменную состояния, а список, поле и всё остальное перерисовываются из неё. “Фотограф умный”: он копирует одинаковые куски кадра и перерисовывает только то, что реально изменилось.
2. JSX это сахар над createElement
JSX выглядит как HTML, но это обычный JavaScript. Компилятор превращает его в вызовы функции. Вот что ты пишешь и во что это раскрывается по схеме automatic runtime:
// что пишешь ты
const node = <button className="primary">send</button>;
// во что это превращает компилятор (jsx-runtime automatic)
import { jsx as _jsx } from 'react/jsx-runtime';
const node = _jsx('button', { className: 'primary', children: 'send' });
Никакой магии: тег это первый аргумент, атрибуты и children это второй.
В React 19 с automatic runtime тебе даже не нужно писать import React:
компилятор сам добавит импорт из react/jsx-runtime.
JSX это не HTML, поэтому есть отличия, на которых спотыкаются в первый день:
classNameвместоclassиhtmlForвместоfor(словаclassиforзарезервированы в JavaScript).- события в camelCase:
onClick,onChange,onSubmit. styleпринимает объект, не строку:style={{ color: 'red' }}.- любой тег либо имеет закрывающую пару, либо самозакрывается:
<br />. - у компонента один корневой узел. Если нужно вернуть несколько узлов
без обёртки, используй пустой фрагмент
<>...</>. - внутри
{ }живёт любое JavaScript-выражение:{user.name},{2 + 2},{items.map(...)}.
const name = 'Вася';
const greeting = (
<>
<h1>Привет, {name}</h1>
<p>Сегодня {new Date().toLocaleDateString('ru')}</p>
</>
);
3. Компоненты, props и children
Компонент
это функция. Она принимает объект props
и возвращает JSX. Имя обязательно с большой буквы: <message /> для JSX
это HTML-тег, а <Message /> это твой компонент.
type MessageProps = {
author: string;
text: string;
};
function Message({ author, text }: MessageProps) {
return (
<li className="message">
<strong>{author}</strong>: {text}
</li>
);
}
Передаём props как атрибуты, а вложенный контент попадает в специальный
prop children:
type CardProps = {
title: string;
children: React.ReactNode;
};
function Card({ title, children }: CardProps) {
return (
<section className="card">
<h2>{title}</h2>
{children}
</section>
);
}
// использование
<Card title="Каналы">
<Message author="Вася" text="привет" />
</Card>;
Поток данных в React однонаправленный: props идут сверху вниз, от
родителя к ребёнку, и менять их ребёнок не может. Если ребёнку нужно
сообщить наверх о событии, родитель передаёт ему функцию-колбэк
(увидишь это в Echo через onClick).
4. useState и события
Props приходят снаружи и неизменны. А как компоненту помнить что-то своё
между перерисовками, например текущее значение счётчика? Для этого есть
хуки,
и первый из них это useState.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter">
<button type="button" onClick={() => setCount(count - 1)}>
-1
</button>
<output>{count}</output>
<button type="button" onClick={() => setCount(count + 1)}>
+1
</button>
</div>
);
}
useState(0) возвращает пару: текущее значение и функцию-сеттер.
Деструктурируем её в count и setCount. Ключевая идея: вызов
setCount не меняет переменную на месте, он просит React
перерисовать компонент с новым значением. Каждый рендер видит свой
снимок count, замороженный на момент отрисовки.
Из этого следует частая ловушка. Если в одном обработчике вызвать сеттер дважды подряд от текущего значения, второй вызов не увидит первый:
// оба вызова видят один и тот же count, итог +1, а не +2
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
// функциональный апдейтер видит свежее значение, итог +2
onClick={() => {
setCount((c) => c + 1);
setCount((c) => c + 1);
}}
Глубже про снимок рендера и замыкания будет в уроке про хуки. Пока запомни правило: если новое состояние зависит от старого, передавай в сеттер функцию.
Сравни controlled input с vanilla-версией. В уроке 02 ты читал
input.value напрямую и сам чистил поле. Здесь источник правды это
состояние, а DOM следует за ним:
function Composer() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="написать сообщение"
/>
);
}
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.