Раздел 7 · Браузер и Frontend

React: декларативный UI, JSX и первый компонент

middle~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

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.