Зачем фреймворк поверх React
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Всё, что вы написали в этой части, держится на одной строке: createRoot(...).render(<App />). Она вешает приложение на пустой узел уже готовой страницы — и в этом же её граница. Второго адреса у приложения нет, до выполнения скрипта на экране нет ничего, а до браузера код не доедет без сборки. Фреймворк начинается ровно там, где эта строка заканчивается.
React рисует в узел, который на странице уже есть. Значит, страница откуда-то берётся — и в приложении на одном React она выглядит одинаково у всех: <div id="root"></div>, тег скрипта с бандлом и больше ничего. Всё остальное, что увидит посетитель, — результат выполнения этого скрипта.
Насколько «ничего» — видно прямо в песочнице: у неё в разметке ровно тот же пустой #root. Прежде чем жать Run, решите, что напечатает строка «сразу после render» — разметку с заголовком или пустоту.
Пусто оба раза. Первый — понятно: скрипт только начался. Второй интереснее: render() ничего не рисует, а ставит работу в очередь — это тот самый триггер из статьи «Триггер, рендер, коммит». Разметка появляется позже, когда React дойдёт до коммита. На странице с настоящим бандлом к этим миллисекундам добавляется скачивание и разбор нескольких сотен килобайт кода.
| Шаг | Что на экране |
|---|---|
| Пришёл HTML | пусто: в разметке один div без содержимого |
| Скачивается бандл | пусто |
| Бандл разбирается и выполняется | пусто |
| Первая отрисовка | каркас и заглушки «Загрузка…» |
| Ушёл и вернулся запрос за данными | наконец содержимое |
На ноутбуке разработчика с локальным сервером эти шаги сливаются в один кадр. На телефоне в метро между первым и последним — секунды белого экрана, и каждый шаг ждёт предыдущего: запрос за данными нельзя начать, пока не выполнился код, который его отправляет.
Вторая половина беды в том, что страницу читают не только люди. Карточка ссылки в мессенджере, превью в соцсети, парсер прайсов, поисковый робот — все они берут то, что пришло в HTML. Гуглбот скрипты выполняет, но откладывает это на отдельный проход; остальные чаще всего не выполняют вовсе. В пустом #root им читать нечего.
Проверить чужое приложение просто: «Просмотр кода страницы» в контекстном меню, а не инспектор. Инспектор показывает уже собранное дерево, а исходный код — то самое, что пришло по сети.
Они лежат в том же единственном index.html — значит, одни на все адреса приложения. Из кода их поменять можно (document.title, запись в <meta>), но случится это уже после выполнения скрипта, а карточка ссылки собирается раньше и по исходному HTML. Поэтому у клиентских приложений все ссылки в мессенджере выглядят одинаково: одно название, одно описание, одна картинка.
Фреймворк решает это тем, что HTML для каждого адреса собирается на сервере — вместе с его метаданными. Как это устроено в Next.js, разбирает статья «Метаданные, sitemap и картинки для соцсетей» в главе про кеш и прод.
Пересчитайте словарь, который вы выучили за эту часть: компоненты, пропсы, состояние, эффекты, контекст, один корень. В нём нет слов «адрес», «сервер», «файл», «сборка». React — библиотека рендеринга: она превращает состояние в дерево узлов и обновляет его. На этом её обязанности заканчиваются.
| Задача живого приложения | Кто её решает, если у вас только React |
|---|---|
Показывать разное на /tasks и /tasks/42 | вы: свой роутер |
Ответить на /tasks/42, когда его открыли напрямую | вы: настройка сервера |
| Отдать первый HTML не пустым | вы: свой серверный рендер |
| Разрезать бандл, чтобы не грузить весь код сразу | вы: настройка сборщика |
| Начать загрузку данных до отрисовки, а не после | вы: своя схема |
| Свой заголовок и описание у каждого адреса | вы |
| Ключи и запросы, которым нельзя в браузер | вы: отдельный сервер |
Это не претензия. Библиотека сознательно не лезет туда, где ответ зависит от проекта: React живёт и в приложении на весь экран, и в одном виджете внутри чужой страницы, и в мобильном React Native, где никакого HTML нет вовсе. Зато и документация React честно советует новое приложение начинать с фреймворка — не из вежливости, а потому что перечисленное всё равно придётся собрать.
А часть пунктов библиотека закрыть и не может. Серверные компоненты, действия сервера, потоковая отдача — это не функции, которые «достаточно вызвать»: они требуют, чтобы кто-то один одновременно владел сборкой, сервером и клиентом, знал, какой кусок кода куда уехал, и умел передать между ними готовый результат. React определяет протокол, а реализует его фреймворк. Поэтому «взять React и добрать нужное по вкусу» упирается не в лень, а в границу возможного.
Можно, и годами так и делали: роутер отдельным пакетом, сборщик настраивается руками, сервер на Express рендерит разметку, к нему прикручивается загрузка данных и разрезание бандла. Собирается это за неделю, и первые полгода работает.
Цена приходит позже. Обновился React — связка рассыпалась в трёх местах сразу. Нужен новый человек в команду — учить его придётся не React, а вашей сборке, про которую нет ни статьи, ни ответа на Stack Overflow. В итоге получается фреймворк, только без документации и с единственным сопровождающим, который однажды уйдёт.
Возьмём самое частое требование: два экрана и адрес, который можно скинуть коллеге. В React для этого нет ничего — значит, пишем сами. Инструменты знакомы: адрес меняет history.pushState, о нажатой кнопке «назад» сообщает событие popstate на window, а хранить текущий путь будет обычное состояние.
Настоящий переход тут не показать: у песочницы нет своего адреса, pushState в изолированном кадре запрещён. Зато сопоставление проверяется без адресной строки — и уже оно ломается на втором вызове.
Вторая строка и есть первая трещина. Адрес /tasks/42 в списке маршрутов не найден, потому что строки сравниваются целиком, а номер задачи заранее неизвестен. Значит, нужен разбор пути на сегменты, потом — параметры запроса после ?, потом — вложенность. Так и начинается свой роутер: с тридцати строк, которые честно работают на двух экранах.
| Что роутер обязан уметь | Что придётся дописать самому |
|---|---|
| Адреса с переменной частью | разбор сегментов и передача id в компонент |
| Прокрутку | наверх при переходе — и обратно на прежнее место по кнопке «назад» |
| Доступность | перенос фокуса и объявление нового экрана для скринридера |
| Незнакомый адрес | свой экран 404 — и правильный код ответа, а не 200 на всё |
| Общую шапку | раскладка, которая при переходе не пересоздаётся и не теряет состояние |
| Вес | разрезание бандла по экранам: иначе первый заход тянет код всех страниц |
| Данные | запрос после отрисовки — значит, заглушка на каждом переходе |
Но даже когда всё это написано, остаётся пункт, который клиентский код не закроет в принципе. Посетитель стоит на /tasks/42 и нажимает F5. Браузер идёт с этим адресом на сервер, сервер ищет такой файл или папку и не находит — 404. Ваш роутер к этому моменту ещё не существует: его код лежит в бандле, а бандл подключён из index.html, который сервер как раз и не отдал.
Лечится это на сервере: отдавать index.html на любой неизвестный адрес. То есть настройкой, которая не живёт нигде в вашем React-приложении — её пишут отдельно в конфиге хостинга, и она первой теряется при переезде. В Next.js этой проблемы не решают, а не создают: маршрут — это папка в проекте, и сервер знает про него из той же файловой структуры. Как именно — следующая статья главы.
Между вашим файлом и браузером обязательно кто-то стоит. Причина первая — синтаксис: <h1>Привет</h1> посреди кода для любого движка JavaScript синтаксическая ошибка, а аннотации типов из части III движок не понимает тем более. И то и другое существует только до сборки.
Причина вторая — импорты. Строку import { useState } from "react" браузер разобрать может, а выполнить нет: голые имена пакетов он без карты импортов — тега, где каждому имени прописан адрес, — не разрешает, ему нужен путь к файлу. Кто-то должен пройти по всем импортам, найти пакеты в node_modules и склеить это в файлы, которые можно отдать по сети.
| Что делает сборка | Что будет без неё |
|---|---|
| Переводит JSX и типы в обычный JavaScript | ошибка на первом же теге в коде |
| Разрешает импорты пакетов | браузер не знает, где искать "react" |
| Склеивает и режет на куски | либо сотни запросов, либо один кусок на всё приложение |
| Ставит хеш в имя файла | либо устаревший кеш у посетителя, либо перекачка каждый раз |
| Подставляет переменные окружения | адрес API прописан прямо в коде |
| Держит dev-сервер с горячей заменой | перезагрузка страницы на каждую правку |
Эта половина закрывается и без фреймворка: npm create vite даёт сборку, dev-сервер и горячую замену за одну команду, и для виджета, админки или внутренней панели этого правда достаточно. Но Vite намеренно останавливается на границе браузера: он готовит файлы. Кто отдаст первый HTML, что ответит на /tasks/42, где выполнится код, которому нельзя в браузер, — вопросы не к нему. Сервер приносит фреймворк сверху; на том же Vite, кстати, работает и React Router в режиме фреймворка. А в Next.js эту половину закрывает встроенный сборщик Turbopack — как именно, разобрано в главе про сборку.
Главное в Next.js не количество возможностей, а то, что решения уже приняты и одинаковы во всех проектах. Папка app — это карта адресов. Файл page.tsx внутри неё — страница. Компонент по умолчанию выполняется на сервере, а в браузер уезжает только то, что вы туда отправили сами.
| Файл | Что это |
|---|---|
app/layout.tsx | общая обёртка: html, body, шапка, меню |
app/page.tsx | страница по адресу / |
app/tasks/page.tsx | страница по адресу /tasks |
Сама страница — обычный компонент, ровно такой же, какие вы писали всю часть. Ни регистрации маршрута, ни конфигурации: важно только имя файла и место в дереве папок.
А дальше эта договорённость окупается на каждом пункте из списка, с которого мы начали.
| Чего нет в React | Что делает Next.js | Где в учебнике |
|---|---|---|
| Адреса и переходы | маршрут — папка, переход — компонент Link | эта глава |
| Пустой первый HTML | разметка приходит с сервера готовой и дорисовывается частями | глава про серверные компоненты |
| Сборка | Turbopack настроен заранее: JSX, TypeScript, стили, разрезание по маршрутам | работает с первой команды |
| Код, которому нельзя в браузер | серверные компоненты: ключи и запросы к базе остаются на сервере | глава про серверные компоненты, статья «Серверный компонент: код, который не уехал в браузер» |
| Отправка формы | действия сервера: функция на сервере вызывается прямо из формы | глава про изменение данных, статья «Server Actions: форма без fetch» |
| Кеш и скорость | статика, ревалидация, кеширование по частям | глава про кеш и прод, статья «Статика, динамика и всё между ними» |
| Заголовки, sitemap, картинки для соцсетей | метаданные объявляются рядом со страницей | глава про кеш и прод |
Ни один пункт не магия: каждый делается руками, и раньше именно так и делали. Вопрос в том, сколько из них вы готовы сопровождать сами — и понадобится ли объяснять это тому, кто придёт в проект после вас.
За снятые заботы платят тремя счетами. Первый — сервер: приложение на Next.js по умолчанию не файлы, а процесс Node, который где-то работает, что-то ест и иногда падает. Статический экспорт из главы про кеш и прод возвращает вас к обычным файлам, но вместе с сервером отключается половина возможностей.
Второй счёт — договорённости. Имя файла теперь значит больше, чем его содержимое, и «серверный это компонент или клиентский» становится вопросом к каждому новому файлу. Третий — скорость изменений: Next за несколько лет дважды заметно переучил своих пользователей — сначала переездом на App Router, затем правилами кеширования, — и половина статей в интернете описывает уже не тот фреймворк.
Поэтому фреймворк нужен не всегда. Виджет расчёта доставки, встраиваемый в чужую страницу; админка за логином, которую не индексируют и открывают с рабочего ноутбука; панель внутри Electron или расширения браузера; один интерактивный кусок на в остальном обычном сайте — во всех этих случаях пустой первый HTML никому не мешает, а сервер только добавит хлопот. Vite и роутер отдельным пакетом честнее.
Правило простое. Если страницы должны находиться поиском, открываться по прямой ссылке, быстро показываться на телефоне и собираться из данных, которые нельзя отдавать браузеру, — берите фреймворк. Если приложение и так начинается с формы входа и живёт на одном экране — не берите.
Next.js не единственный: React Router и TanStack Start решают тот же список. В учебнике мы берём Next.js, потому что серверные возможности React обкатываются вместе с ним — и потому что на нём собран сайт, который вы сейчас читаете.
- React рисует в уже существующий узел: страница, адрес, сервер и сборка в его обязанности не входят по замыслу.
- Клиентское приложение отдаёт браузеру пустой контейнер, и всё содержимое появляется только после загрузки и выполнения бандла — со всеми последствиями для скорости, ссылок и роботов.
- Свой роутер пишется за вечер и упирается в сервер: прямой заход на
/tasks/42клиентский код перехватить не успевает. - JSX, типы и импорты из пакетов до браузера не доезжают — шаг сборки обязателен всегда, и Vite закрывает ровно его.
- Next.js закрывает список целиком, потому что владеет и сборкой, и сервером: адрес — это папка, серверные возможности React без такого хозяина невозможны в принципе.
- Цена — процесс на сервере, чужие договорённости и скорость изменений; за формой входа и в виджете фреймворк не нужен.
1/4Приложение на одном React выложено на статический хостинг. Посетитель стоит на /tasks/42 и нажимает F5. Что произойдёт, если хостинг не настраивали?
2/4Сразу после root.render(<Hello />) код читает root.innerHTML. Что там окажется?
3/4Ссылку на клиентское React-приложение отправили в мессенджер, и карточка получилась пустой. Почему?
4/4Что из перечисленного нельзя добавить в React отдельным пакетом, не привлекая фреймворк?
Сосчитать шаги до первого слова
Клиентское приложение: пустой index.html, бандл на 300 КБ, список задач загружается запросом из useEffect. Выпишите по порядку всё, что должно случиться, прежде чем посетитель увидит первую задачу. Затем отметьте, какие из этих шагов исчезают, если сервер отдаст сразу готовый HTML со списком.
Шесть шагов, и каждый ждёт предыдущего:
1. запрос за адресом — сервер отдаёт HTML с пустым #root;
2. браузер видит тег скрипта и запрашивает бандл;
3. бандл едет по сети (на медленной мобильной связи это самый долгий шаг);
4. движок разбирает и выполняет код, React монтирует дерево;
5. первая отрисовка — на экране заглушка «Загрузка…»;
6. эффект отправляет запрос за задачами, ответ возвращается, происходит вторая отрисовка.
Если HTML приходит с сервера готовым, шаги 5 и 6 из ожидания пропадают: список виден сразу после первого запроса, а бандл догружается уже поверх видимой страницы. Шаги 2–4 остаются — интерактив всё равно включается кодом, — но пользователь их не ждёт, а читает.
Отсюда и правило: убирать надо не килобайты, а ожидания в цепочке. Именно поэтому первый HTML — задача фреймворка, а не сборщика.
Адрес с переменной частью
Наивный матчер сравнивает строки целиком, поэтому шаблон /tasks/:id не совпадает ни с чем. Научите его понимать сегменты, начинающиеся с двоеточия: такой сегмент подходит к любому непустому значению, а само значение попадает в params. Число сегментов должно совпадать.
Разбиваем оба пути на сегменты и идём по ним парами. Сегмент-шаблон с двоеточием забирает значение в params, обычный — обязан совпасть буквально.
Двадцать строк — и это только начало. Дальше понадобятся необязательные сегменты, «хвост» произвольной длины, приоритет между /tasks/new и /tasks/:id, регистр, слеш на конце. Каждый пункт — известная ловушка с известным решением, и переписывать их в каждом проекте заново незачем.
Нужен ли здесь фреймворк
Три проекта. Для каждого решите, брать Next.js или хватит сборщика с роутером, и назовите решающий довод — он в каждом случае один.
А. Сайт студии: услуги, блог, страницы кейсов, форма заявки.
Б. Панель мониторинга серверов: открывается за VPN после входа, живёт на одном экране, обновляет графики раз в секунду.
В. Виджет расчёта доставки, который партнёры вставляют к себе на страницу одним тегом скрипта.
А — фреймворк. Решающий довод: страницы должны находиться поиском и красиво разворачиваться в мессенджере. Это ровно то, что невозможно с пустым первым HTML и одним заголовком на все адреса.
Б — не нужен. Довод: за формой входа нет ни роботов, ни первого впечатления, а экран один. Пустой HTML на полсекунды тут не видит никто, зато сервер Node пришлось бы куда-то поставить и сопровождать.
В — противопоказан. Довод: страницу отдаёт чужой сайт, вашего HTML в ней нет вовсе. Задача сводится к одному небольшому бандлу, который монтируется в переданный узел, — сборщика достаточно, а весь остальной фреймворк просто некуда приложить.