Первый компонент и первый запуск

зачем React≈ 8 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В части II ваш render() собирал строку и клал её в innerHTML — и звали его вы, руками, после каждой правки данных. Здесь функция вернёт не строку, а описание разметки, и звать её будет React. Разница кажется косметической ровно до того момента, когда вы напечатаете порядок вызовов.

Разделы темы:·

Компонент — это функция. Не класс, не шаблон, не особая сущность фреймворка: обычная функция, объявленная как все остальные. Обязанностей у неё ровно две — называться с заглавной буквы и вернуть то, что можно нарисовать.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Три строки, и все три знакомы. document.getElementById — тот же поиск узла, что и в части II: своего способа найти место на странице React не изобретал. createRoot получает этот узел и отдаёт корень — точку, в которой React берёт кусок дерева под свою ответственность. render сообщает, что в этом куске рисовать.

Разметка прямо внутри функции — это JSX. Выглядит он как HTML и ведёт себя не как HTML; где именно они расходятся, разберём в следующей статье. Пока хватит того, что <p>…</p> в коде — законное выражение языка: его можно положить в переменную и вернуть из функции.

Не «разметку», а всё, что React умеет нарисовать: элемент, строку, число, массив элементов — или null, если рисовать нечего. В типах React это объединение называется ReactNode, и компилятор пропустит любой его вариант.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В песочнице React и ReactDOM глобальные, потому что пример — один файл. В настоящем проекте вверху стоят импорты, но на саму функцию это не влияет.

Контейнер — обычный пустой div в HTML-файле, чаще всего с id="root". Ничего волшебного в нём нет: с этого момента внутренностями узла распоряжается React, а вы туда руками не лезете. Именно поэтому контейнер держат пустым — всё, что в нём было, первый же render заменит.

И сразу ловушка из статьи «null, undefined и strictNullChecks». getElementById обещает HTMLElement | null, а createRoot принимает только узел:

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностика знакомая: Аргумент типа "HTMLElement | null" нельзя назначить параметру типа "DocumentFragment | Element". И обратите внимание на кадр — пример всё равно отработал. Типы стираются при сборке, до браузера доезжает обычный JavaScript, а узел на странице действительно есть. Восклицательный знак в примерах учебника — это обещание «узел на месте», а не проверка.

Корень — объект, а не функция. У него есть render, который можно звать сколько угодно раз, и unmount, который возвращает узел странице.

unmount убирает всё, что React нарисовал, и отпускает контейнер. Пригодится редко — например, когда React живёт кусочком внутри чужого приложения.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В приложении на Next.js этих трёх строк вы не увидите: корень создаёт фреймворк. Но он их не отменяет — просто пишет за вас.

Соглашение «компоненты с большой буквы» звучит как вкусовщина из чужого код-стайла. Оно не вкусовщина: от регистра первой буквы зависит, во что превратится строка разметки.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Два свидетельства сразу. Компилятор говорит: Свойство "hello" не существует в типе "JSX.IntrinsicElements" — то есть среди известных ему тегов такого нет. А React, не зная слова «hello», честно создаёт узел с таким именем: браузер не возражает, показывать в нём нечего. Функция при этом не вызвана ни разу — в выводе от неё пусто.

В кодеЧто понимает JSXЧто получится
<p />имя тегаузел <p>
<hello />имя тега — со строчной буквы других вариантов нетузел <hello>, функция не вызвана
<Hello />ссылка на переменную HelloReact вызовет функцию и нарисует её возврат

Отсюда же и второе правило: компонент должен быть виден по имени в том же файле. Точка тоже годится — Icons.Trash React разбирает как обращение к объекту.

Компонент — обычная функция, значит, её можно позвать самому. Посмотрим, что от этого изменится.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Строка «Hello вызвана» появляется дважды: один раз от вашего вызова и один — от React. Между returned и element разница принципиальная. returned — уже результат: описание абзаца, про Hello в нём не осталось ни следа, в поле type лежит строка "p". А element — заявка: в его type лежит сама функция, и React знает, чем этот кусок дерева рисовать, а значит, сможет вызвать её ещё раз, когда понадобится.

Когда понадобится — решает тоже он. Напечатаем метки до вызова, внутри компонента и после.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Порядок — 1, 3, 2. render не вызывает вашу функцию: он ставит работу в очередь и тут же возвращает управление, а React берётся за неё позже, когда браузер отдаст ему такт. Это ровно тот цикл из статьи «Event loop: как браузер успевает всё», только в очередь кладёт не ваш setTimeout, а планировщик React.

Отсюда setTimeout в примерах этой статьи: чтобы заглянуть в DOM после render, надо дождаться, пока React до него доберётся. В обычном коде так не пишут — там за узлами ходят через ref, о нём в главе про эффекты.

Раз просьба ставится в очередь, попросим трижды подряд и посчитаем вызовы функции.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вызов один. Три просьбы, поступившие до того, как React взялся за работу, схлопываются в одну отрисовку — считать промежуточные состояния незачем, важно только последнее. Ваш render() из части II так не умел: сколько раз позвали, столько раз он и собрал строку.

Теперь главное отличие. Попросим два раза с паузой между ними — и посмотрим, что станет с узлами.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Заголовок сменился, а поле — тот же самый узел: document.getElementById("draft") === field даёт true, и набранный текст никуда не делся. React вызвал функцию заново, получил новое описание, сравнил его с прошлым и тронул в живом дереве только то, что разошлось, — текст внутри h1. До input он не дотронулся, потому что в описании тот не изменился.

Вспомните врезку «Чем платит innerHTML» из статьи «Рендер из состояния и делегирование»: там app.innerHTML = app.innerHTML терял введённое значение, потому что value живёт в узле, а в разметке остаётся только атрибут. Здесь терять нечего — узел не пересоздавался.

render() из части IIroot.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()?

Кадр остался пустым

Пример запускается, компилятор жалуется, а в кадре пусто. Найдите причину и почините — правка на один символ.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Имя со строчной буквы JSX считает именем тега, поэтому React создал узел <greeting>, а функцию не позвал. Достаточно заглавной буквы — в объявлении и в разметке.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Заодно замолкает и компилятор: Greeting — обычная переменная в области видимости, искать её среди известных тегов он больше не станет.

Сменить заголовок, не потеряв ввод

В поле уже набран текст. Допишите код так, чтобы заголовок стал «Готово», а набранное осталось на месте. Трогать innerHTML и сам узел h1 нельзя — только просить React.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Меняем данные и просим отрисовать заново — тем же корнем и тем же компонентом.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

React вызовет Editor второй раз, получит новое описание, сравнит его с прошлым и заменит только текст внутри h1. Узел input в описании не изменился — значит, React его не трогает, и набранное значение остаётся. Ручная пересборка через innerHTML здесь потеряла бы и текст, и фокус, и позицию курсора.

Две записи, один кадр

root.render(Hello()) и root.render(<Hello />) рисуют на экране одно и то же. Напечатайте поле type у обоих значений и скажите, что именно получил React в каждом случае.

tsx
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У первого в type лежит строка с именем тега, у второго — сама функция.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

В первом случае React получает готовое описание абзаца и о существовании Hello не узнаёт: вызвать её заново ему нечем. Во втором он знает, какой функцией нарисован этот кусок дерева, и может позвать её столько раз, сколько потребуется. Пока состояния нет, разница не видна; с первым же useState она станет разницей между работающим компонентом и картинкой.