Первый компонент и первый запуск
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
В части II ваш render() собирал строку и клал её в innerHTML — и звали его вы, руками, после каждой правки данных. Здесь функция вернёт не строку, а описание разметки, и звать её будет React. Разница кажется косметической ровно до того момента, когда вы напечатаете порядок вызовов.
Компонент — это функция. Не класс, не шаблон, не особая сущность фреймворка: обычная функция, объявленная как все остальные. Обязанностей у неё ровно две — называться с заглавной буквы и вернуть то, что можно нарисовать.
Три строки, и все три знакомы. document.getElementById — тот же поиск узла, что и в части II: своего способа найти место на странице React не изобретал. createRoot получает этот узел и отдаёт корень — точку, в которой React берёт кусок дерева под свою ответственность. render сообщает, что в этом куске рисовать.
Разметка прямо внутри функции — это JSX. Выглядит он как HTML и ведёт себя не как HTML; где именно они расходятся, разберём в следующей статье. Пока хватит того, что <p>…</p> в коде — законное выражение языка: его можно положить в переменную и вернуть из функции.
Не «разметку», а всё, что React умеет нарисовать: элемент, строку, число, массив элементов — или null, если рисовать нечего. В типах React это объединение называется ReactNode, и компилятор пропустит любой его вариант.
В песочнице React и ReactDOM глобальные, потому что пример — один файл. В настоящем проекте вверху стоят импорты, но на саму функцию это не влияет.
Контейнер — обычный пустой div в HTML-файле, чаще всего с id="root". Ничего волшебного в нём нет: с этого момента внутренностями узла распоряжается React, а вы туда руками не лезете. Именно поэтому контейнер держат пустым — всё, что в нём было, первый же render заменит.
И сразу ловушка из статьи «null, undefined и strictNullChecks». getElementById обещает HTMLElement | null, а createRoot принимает только узел:
Диагностика знакомая: Аргумент типа "HTMLElement | null" нельзя назначить параметру типа "DocumentFragment | Element". И обратите внимание на кадр — пример всё равно отработал. Типы стираются при сборке, до браузера доезжает обычный JavaScript, а узел на странице действительно есть. Восклицательный знак в примерах учебника — это обещание «узел на месте», а не проверка.
Корень — объект, а не функция. У него есть render, который можно звать сколько угодно раз, и unmount, который возвращает узел странице.
unmount убирает всё, что React нарисовал, и отпускает контейнер. Пригодится редко — например, когда React живёт кусочком внутри чужого приложения.
В приложении на Next.js этих трёх строк вы не увидите: корень создаёт фреймворк. Но он их не отменяет — просто пишет за вас.
Соглашение «компоненты с большой буквы» звучит как вкусовщина из чужого код-стайла. Оно не вкусовщина: от регистра первой буквы зависит, во что превратится строка разметки.
Два свидетельства сразу. Компилятор говорит: Свойство "hello" не существует в типе "JSX.IntrinsicElements" — то есть среди известных ему тегов такого нет. А React, не зная слова «hello», честно создаёт узел с таким именем: браузер не возражает, показывать в нём нечего. Функция при этом не вызвана ни разу — в выводе от неё пусто.
| В коде | Что понимает JSX | Что получится |
|---|---|---|
<p /> | имя тега | узел <p> |
<hello /> | имя тега — со строчной буквы других вариантов нет | узел <hello>, функция не вызвана |
<Hello /> | ссылка на переменную Hello | React вызовет функцию и нарисует её возврат |
Отсюда же и второе правило: компонент должен быть виден по имени в том же файле. Точка тоже годится — Icons.Trash React разбирает как обращение к объекту.
Компонент — обычная функция, значит, её можно позвать самому. Посмотрим, что от этого изменится.
Строка «Hello вызвана» появляется дважды: один раз от вашего вызова и один — от React. Между returned и element разница принципиальная. returned — уже результат: описание абзаца, про Hello в нём не осталось ни следа, в поле type лежит строка "p". А element — заявка: в его type лежит сама функция, и React знает, чем этот кусок дерева рисовать, а значит, сможет вызвать её ещё раз, когда понадобится.
Когда понадобится — решает тоже он. Напечатаем метки до вызова, внутри компонента и после.
Порядок — 1, 3, 2. render не вызывает вашу функцию: он ставит работу в очередь и тут же возвращает управление, а React берётся за неё позже, когда браузер отдаст ему такт. Это ровно тот цикл из статьи «Event loop: как браузер успевает всё», только в очередь кладёт не ваш setTimeout, а планировщик React.
Отсюда setTimeout в примерах этой статьи: чтобы заглянуть в DOM после render, надо дождаться, пока React до него доберётся. В обычном коде так не пишут — там за узлами ходят через ref, о нём в главе про эффекты.
Раз просьба ставится в очередь, попросим трижды подряд и посчитаем вызовы функции.
Вызов один. Три просьбы, поступившие до того, как React взялся за работу, схлопываются в одну отрисовку — считать промежуточные состояния незачем, важно только последнее. Ваш render() из части II так не умел: сколько раз позвали, столько раз он и собрал строку.
Теперь главное отличие. Попросим два раза с паузой между ними — и посмотрим, что станет с узлами.
Заголовок сменился, а поле — тот же самый узел: document.getElementById("draft") === field даёт true, и набранный текст никуда не делся. React вызвал функцию заново, получил новое описание, сравнил его с прошлым и тронул в живом дереве только то, что разошлось, — текст внутри h1. До input он не дотронулся, потому что в описании тот не изменился.
Вспомните врезку «Чем платит innerHTML» из статьи «Рендер из состояния и делегирование»: там app.innerHTML = app.innerHTML терял введённое значение, потому что value живёт в узле, а в разметке остаётся только атрибут. Здесь терять нечего — узел не пересоздавался.
| render() из части II | root.render() | |
|---|---|---|
| Что уходит наружу | строка разметки | объект-описание |
| Старые узлы | выбрасываются все | остаются, если совпали по месту и типу |
| Введённый текст, фокус, прокрутка | теряются | на месте |
| Кто зовёт функцию | вы, после каждой правки данных | React, когда сочтёт нужным |
| Две правки подряд | две пересборки | одна отрисовка |
Чтение внешней переменной title из компонента — костыль до главы про состояние: так писать не стоит. В следующей главе разметка начнёт зависеть от памяти самого компонента, и звать render руками больше не придётся.
- Компонент — обычная функция с заглавной буквы, возвращающая элемент, строку, число или
null. createRoot(узел)отдаёт кусок страницы под ответственность React,root.render(элемент)говорит, что там рисовать.- Строчная буква в
<hello />— это имя тега: React создаст узел<hello>, а функцию не вызовет. <Hello />ничего не вызывает: это описание, в полеtypeкоторого лежит сама функция.renderставит работу в очередь и сразу возвращает управление — компонент выполняется позже, а несколько просьб из одного такта дают одну отрисовку.- Повторный
renderне пересобирает разметку: React сравнивает описания и меняет только разошедшееся, поэтому ввод, фокус и прокрутка остаются на месте.
1/4Функция названа со строчной буквы: function hello() {…}, а в разметке написано <hello />. Что окажется на странице?
2/4До root.render(<Clock />) печатается «1», внутри Clock — «2», сразу после вызова render — «3». В каком порядке выйдут строки?
3/4root.render(<Hello />) вызван три раза подряд, строка за строкой. Сколько раз выполнится тело функции Hello?
4/4Чем <Hello /> отличается от Hello()?
Кадр остался пустым
Пример запускается, компилятор жалуется, а в кадре пусто. Найдите причину и почините — правка на один символ.
Имя со строчной буквы JSX считает именем тега, поэтому React создал узел <greeting>, а функцию не позвал. Достаточно заглавной буквы — в объявлении и в разметке.
Заодно замолкает и компилятор: Greeting — обычная переменная в области видимости, искать её среди известных тегов он больше не станет.
Сменить заголовок, не потеряв ввод
В поле уже набран текст. Допишите код так, чтобы заголовок стал «Готово», а набранное осталось на месте. Трогать innerHTML и сам узел h1 нельзя — только просить React.
Меняем данные и просим отрисовать заново — тем же корнем и тем же компонентом.
React вызовет Editor второй раз, получит новое описание, сравнит его с прошлым и заменит только текст внутри h1. Узел input в описании не изменился — значит, React его не трогает, и набранное значение остаётся. Ручная пересборка через innerHTML здесь потеряла бы и текст, и фокус, и позицию курсора.
Две записи, один кадр
root.render(Hello()) и root.render(<Hello />) рисуют на экране одно и то же. Напечатайте поле type у обоих значений и скажите, что именно получил React в каждом случае.
У первого в type лежит строка с именем тега, у второго — сама функция.
В первом случае React получает готовое описание абзаца и о существовании Hello не узнаёт: вызвать её заново ему нечем. Во втором он знает, какой функцией нарисован этот кусок дерева, и может позвать её столько раз, сколько потребуется. Пока состояния нет, разница не видна; с первым же useState она станет разницей между работающим компонентом и картинкой.