React: компоненты вместо ручного DOM
- Зачем это нужно
- Идея: описание вместо шагов
- JSX и во что он превращается
- Компонент: функция, возвращающая описание
- Свойства: данные вниз, события вверх
- Условия в описании
- Тот же список, но на React
- Сколько это стоит
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Прошлая глава закончилась измерением. Список из двухсот строк, изменилось одно число:
- перерисовать всё — 33 миллисекунды, и пользователь теряет набранный текст;
- поменять одну ячейку — быстро и безопасно, но таких мест в настоящем интерфейсе десятки, и каждое надо помнить.
Библиотека решает ровно эту задачу. Вы описываете, как интерфейс должен выглядеть при таких данных. Она сама сравнивает новое описание со старым и трогает только изменившееся.
Дальше в этой главе — измерение того же списка на React, и оно объясняет, почему за этим подходом пошла вся отрасль.
Идея: описание вместо шагов
Ручной подход — последовательность действий: найди ячейку, поставь новое значение; найди строку, добавь класс; удали лишние узлы.
Подход React — функция от данных к виду. Вы пишете: «при таких данных интерфейс выглядит так». Изменились данные — функция вызывается снова и возвращает новое описание. Что с этим описанием делать, решает библиотека.
Ключевая разница в том, чего вы не пишете: ни одной строки про «найти и заменить». Их не бывает вовсе.
JSX и во что он превращается
Внутри кода пишут разметку. Выглядит странно — это не разметка:
function Balance({ amount, onWithdraw }) {
return (
<div className="card">
<b>{amount} ETH</b>
<button onClick={onWithdraw} disabled={amount === 0}>ok</button>
</div>
);
}
Прогон пропускает этот текст через настоящий сборщик и печатает результат:
── во что это превращается ──
function Balance({ amount, onWithdraw }) {
return _jsxs("div", {
className: "card",
children: [
_jsxs("b", { children: [amount, " ETH"] }),
_jsx("button", { onClick: onWithdraw, disabled: amount === 0, children: "ok" })
]
});
}
JSX — это сокращённая запись вызовов функции. Сборка превращает его в обычный код до того, как он попадёт в браузер.
Из этого сразу следуют все «странные» правила:
Результат вызова — обычный объект, описывающий, что должно быть на экране. Это не элемент страницы: у него нельзя спросить размеры и нельзя навесить обработчик напрямую. Элементы появятся, когда библиотека сравнит описание с тем, что уже нарисовано.
Компонент: функция, возвращающая описание
function Balance({ amount }) {
return <b>{amount} ETH</b>;
}
Всё. Компонент — это функция, которая принимает данные и возвращает описание. Три правила:
- Имя с большой буквы. По нему запись отличает ваш компонент от обычного тега:
<Balance />против<b>. Со строчной буквы он будет понят как неизвестный тег. - Возвращает одно описание. Несколько элементов подряд заворачивают в общего родителя или в пустую обёртку
<>…</>. - Ничего не меняет снаружи. Функция вызывается когда угодно и сколько угодно раз — она должна лишь считать результат по входным данным.
<div>
<Balance amount={12} />
<Balance amount={0} />
</div>
Один и тот же компонент, разные данные — разный результат. Это и есть повторное использование, ради которого всё затевалось.
Свойства: данные вниз, события вверх
Свойства только на чтение. Потомок не может изменить то, что ему передали. Если ему нужно что-то поменять — родитель передаёт ему функцию, и потомок её зовёт.
function OpsList({ ops, onWithdraw }) {
return (
<ul>
{ops.map((op) => (
<li key={op.id}>
{op.who} — {op.amount} ETH
<button onClick={() => onWithdraw(op.id)}>Забрать</button>
</li>
))}
</ul>
);
}
Про ключи отдельно. Ключ должен быть устойчивым идентификатором данных, а не номером по порядку. Номер меняется при вставке и удалении, и библиотека начинает считать, что изменилось содержимое строк, а не их состав, — с потерей введённого текста и неправильной подсветкой.
Условия в описании
Отдельных «если» в записи нет — она умеет только выражения. Поэтому пользуются тремя приёмами:
{isLoading && <Spinner />}
{amount > 0 ? <b>{amount} ETH</b> : <span>пусто</span>}
{ops.length === 0 && <p>Операций пока нет</p>}
Когда веток становится больше трёх, условие выносят из описания: считают результат в переменную выше и подставляют её.
Тот же список, но на React
Прогон повторяет опыт прошлой главы: двести строк, в каждой поле ввода. Пользователь набрал текст, и в это время меняется одно число в первой строке.
узлов li в списке: 200
число в первой строке до: 1
функций-строк вызвано заново: 200 из 200
число в первой строке после: 42
тот же ли узел li : true
тот же ли узел b : true
тот же ли узел input : true
фокус остался в поле : true
текст в поле : "моя заметка"
Сравните с прошлой главой: там после перерисовки было фокус false, текст "".
Функции строк вызвались все двести. Это важно понять правильно: пересчёт описания — дешёвая операция, она происходит целиком. Дорогая операция — изменение страницы, и её библиотека делает точечно.
Ни один узел страницы не был пересоздан. Библиотека сравнила старое описание с новым, увидела, что отличается один текстовый узел, и поменяла только его.
Сколько это стоит
перерисовка списка из 200 строк после изменения одного числа
среднее : 3.46 мс
медиана : 2.10 мс
минимум : 1.00 мс
Против 33,27 миллисекунды на ручную перерисовку — в пятнадцать раз быстрее. И это при том, что все двести функций пересчитались.
Вывод, который стоит запомнить: «перерисовка» в React — это не перерисовка страницы. Это пересчёт описания. Страница меняется ровно там, где описание разошлось со старым.
Из этого следует и обратное: если компонент делает при вызове что-то тяжёлое или что-то с побочным действием — запрос в сеть, запись в хранилище, — это будет происходить при каждом пересчёте. Вот почему компонент обязан быть просто вычислением. Куда девать всё остальное — тема следующей главы.
Типичные ошибки
onClick={handler(id)} вместо стрелки. Функция вызывается во время описания; обработчику достаётся её результат. Если внутри было изменение состояния — получится бесконечный цикл.
Перенос строки после return без скобок. Инструкция завершается, функция возвращает пустоту, на экране ничего.
Компонент с маленькой буквы. Понимается как неизвестный тег и молча ничего не рисует.
Ключ — номер по порядку. Работает, пока список не меняется в середине.
{ops.length && <Список/>}. При пустом списке на экране появляется ноль.
Изменение свойств внутри потомка. Свойства только на чтение; менять данные может лишь тот, кто ими владеет.
class вместо className. Предупреждение в консоли, оформление не применяется.
Побочные действия прямо в теле компонента. Запрос в сеть при каждом пересчёте, а пересчёт бывает часто.
Слова главы
- Компонент — функция от данных к описанию интерфейса; имя с большой буквы.
- Описание — обычный объект, а не элемент страницы; элементы появляются позже.
- JSX — сокращённая запись вызовов; исчезает при сборке.
- Свойства — данные, переданные сверху; только на чтение.
- Обработчик из родителя — способ потомка сообщить наверх, что произошло.
- Ключ — устойчивый идентификатор элемента списка, а не номер.
- Согласование — сравнение нового описания со старым и точечное изменение страницы.
- Пересчёт — вызов функций компонентов; дёшев и происходит целиком.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
function Balance({ amount }) { return <b>{amount} ETH</b>; }
На чемпионате
Разбейте интерфейс на компоненты до того, как писать код. Баланс, форма вклада, список операций, полоска состояния сети. Четыре файла вместо одного — и правки перестают задевать чужое.
Свойства называйте по смыслу данных, а не по виду. amount, а не text: через час станет понятно, зачем.
Ключ — идентификатор из данных. Для операций это хеш транзакции: он уникален и не меняется.
Не бойтесь «лишних» пересчётов. Оптимизация до того, как измерено, — потерянное время. Двести строк пересчитываются за две миллисекунды.
И практическое: держите компоненты маленькими. Компонент на двести строк невозможно понять на защите, а разбить его на четыре по пятьдесят — пять минут.
Куда углубиться
- Пустая обёртка — вернуть несколько элементов без лишнего узла на странице.
- Свойство «дети» — передать содержимое внутрь компонента, как в обычный тег.
- Компонент как значение — передать компонент свойством и нарисовать его в другом месте.
- Строгий режим — почему в разработке всё вызывается дважды и что этим проверяют.
- Инструменты разработчика для React — дерево компонентов и их свойства прямо в браузере.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- React: официальный учебник — от первого компонента до списков и условий.
- React: описание интерфейса — компоненты, свойства, списки, ключи.
- React: Render and Commit — что именно происходит при пересчёте.
- React: JSX в подробностях — правила записи и почему они такие.
- React: сохранение и сброс состояния — роль ключей, разобранная до конца.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.