1. Введение в Next.js и архитектуру App Router: маршрутизация и иерархия компонентов
Введение в Next.js и архитектуру App Router: маршрутизация и иерархия компонентов
Почему переход от чистого React к Next.js часто сравнивают с переездом из мастерской в высокотехнологичный завод? В React вы сами решаете, как организовать папки, какую библиотеку выбрать для роутинга и как склеить компоненты в рабочее приложение. В Next.js архитектура продиктована самой файловой системой. Это не просто «библиотека поверх библиотеки», а смена парадигмы, где маршрут — это не строка в конфигурационном файле, а физическая структура вашего проекта. С выходом App Router в версии 13.4 и далее, эта концепция достигла своего апогея, разделив мир фронтенда на «серверное» и «клиентское» по умолчанию.
Философия App Router и эволюция рендеринга
Долгое время веб-разработка металась между двумя крайностями. Сначала были классические многостраничные приложения (MPA), где сервер генерировал HTML на каждый запрос. Затем пришла эра Single Page Applications (SPA), где браузер получал пустой index.html и огромный JavaScript-бандл, который «оживлял» страницу. Next.js с архитектурой App Router предлагает гибридный путь, базирующийся на React Server Components (RSC).
Главное отличие App Router от предыдущей модели (Pages Router) заключается в том, что теперь каждый компонент по умолчанию является серверным. Это означает, что код компонента исполняется на сервере, генерирует готовый HTML и отправляет его клиенту без лишнего JavaScript. Это радикально снижает показатель Total Blocking Time (TBT) и улучшает SEO, так как поисковые роботы видят контент сразу, а не ждут исполнения скриптов.
Серверные и клиентские компоненты: граница ответственности
Понимание разницы между Server Components и Client Components — это фундамент, без которого невозможно построить эффективное приложение на Next.js.
'use client' в самой первой строке файла. Они гидрируются в браузере и могут использовать интерактивность: useState, useEffect, обработчики событий (onClick) и браузерные API (localStorage, geolocation).Важно понимать, что «клиентский» не означает «рендерящийся только в браузере». Клиентские компоненты в Next.js все равно проходят этап предварительного рендеринга на сервере (SSR) для получения начального HTML, но их логика «оживает» уже на стороне пользователя.
Файловая система как карта маршрутов
В App Router маршрутизация строится на иерархии папок внутри директории app. Если в классическом React-приложении вы используете react-router-dom и описываете пути в компоненте <Routes>, то здесь путь в адресной строке браузера напрямую соответствует пути к папке.
| URL Путь | Путь в файловой системе | Итоговый файл |
| :--- | :--- | :--- |
| / | app/page.tsx | Главная страница |
| /dashboard | app/dashboard/page.tsx | Страница панели управления |
| /dashboard/settings | app/dashboard/settings/page.tsx | Вложенная страница настроек |
| /blog/[slug] | app/blog/[slug]/page.tsx | Динамический маршрут для статьи |
Специальные файлы: скелет приложения
Next.js резервирует определенные имена файлов, которые выполняют специфические роли в иерархии маршрута. Это позволяет декларативно описывать поведение интерфейса.
* page.tsx: Уникальный интерфейс маршрута. Без этого файла папка не будет доступна как публичный URL.
* layout.tsx: Общий интерфейс для сегмента и его дочерних элементов. Состояние лейаута сохраняется при навигации между соседними страницами. Например, если у вас есть боковое меню в лейауте /dashboard, оно не будет перерендериваться при переходе из /dashboard/analytics в /dashboard/settings.
* template.tsx: Похож на лейаут, но создает новый экземпляр компонента при каждой навигации. Это полезно для анимаций входа/выхода или сброса состояния поиска.
* loading.tsx: Автоматически оборачивает содержимое страницы в React Suspense. Пока данные для страницы загружаются, пользователь видит этот компонент (например, скелетон).
* error.tsx: Граница ошибки (Error Boundary). Если в дочерних компонентах произойдет сбой, Next.js отрендерит этот файл вместо «белого экрана смерти».
* not-found.tsx: Обработка ситуации, когда ресурс не найден (404).
Иерархия компонентов и вложенность
Одной из самых мощных функций App Router является вложенная маршрутизация (Nested Routing). Представьте приложение социальной сети. У вас есть глобальный лейаут с шапкой сайта. Внутри него — лейаут профиля с аватаром и вкладками («Посты», «Друзья», «Фото»). А еще глубже — конкретный контент выбранной вкладки.
В Next.js это реализуется через композицию файлов layout.tsx. Каждый вложенный лейаут получает children — это либо следующий вложенный лейаут, либо конечная страница page.tsx.
> «Дизайн системы — это не только визуальный язык, но и структура владения данными. Вложенные лейауты позволяют нам загружать данные на том уровне, где они действительно нужны, не блокируя рендеринг всего приложения». > > Next.js Documentation
Пример структуры сложного маршрута
Рассмотрим структуру для CRM-системы:
В этой схеме при переходе на /dashboard/projects/123:
Root Layout.children вставится Dashboard Layout.Dashboard Layout вставится Project Detail Page.123 еще грузятся, вместо страницы покажется loading.tsx из папки [id].Динамические маршруты и параметры
Часто нам нужно создавать страницы на основе данных: товары в магазине, посты в блоге или профили пользователей. Для этого используются динамические сегменты, которые записываются в квадратных скобках: [slug] или [id].
Next.js передает параметры из URL в компонент страницы через пропс params.
Например, для файла app/shop/[category]/[item]/page.tsx и URL /shop/electronics/iphone, объект params будет выглядеть так:
{ category: 'electronics', item: 'iphone' }.
Обработка неопределенного количества сегментов
Иногда структура URL заранее неизвестна. Например, файловый менеджер или документация с глубокой вложенностью. В этом случае используются "catch-all segments":
* [...slug] — поймает /docs/intro, /docs/intro/setup. Но не поймает /docs.
* [[...slug]] — "optional catch-all". Поймает и /docs, и любую вложенность. В этом случае params.slug будет массивом строк: ['intro', 'setup'].
Навигация и связывание страниц
В Next.js существует два основных способа перехода между маршрутами: компонент <Link> и хук useRouter.
Компонент Link
Это расширение стандартного тега <a>, которое обеспечивает "prefetching" (предзагрузку). Как только ссылка попадает во вьюпорт (видимую область экрана) пользователя, Next.js начинает в фоновом режиме загружать код и данные для этой страницы. Благодаря этому переход кажется мгновенным.
Хук useRouter
Если навигацию нужно выполнить программно (например, после успешной отправки формы), используется хук useRouter из пакета next/navigation. Важно: этот хук работает только в клиентских компонентах.
Группировка маршрутов и частные папки
Иногда структура папок нужна нам для организации кода, а не для создания URL. Next.js предоставляет два инструмента для этого:
(name): Папки, обернутые в круглые скобки, исключаются из URL. Это полезно для разделения приложения на логические секции (например, (auth) для логина/регистрации и (main) для основного контента), чтобы применять к ним разные лейауты, не меняя путь в браузере.app/(auth)/login/page.tsx доступен по адресу /login.
_name: Папки, начинающиеся с нижнего подчеркивания, полностью игнорируются системой маршрутизации. Там удобно хранить компоненты, специфичные для данного раздела, тесты или стили.Глубокое погружение: Механизм рендеринга и кэширования
Одной из самых сложных тем для понимания является то, как Next.js обрабатывает запросы к страницам в App Router. В отличие от Pages Router, где использовались методы getServerSideProps и getStaticProps, здесь все завязано на расширенном API fetch и кэшировании на уровне сегментов.
Рендеринг серверных компонентов (RSC Payload)
Когда пользователь переходит по ссылке, сервер не просто генерирует HTML. Он создает специальное описание дерева компонентов — RSC Payload. Это компактный текстовый формат, который содержит: * Результат рендеринга серверных компонентов. * Плейсхолдеры для клиентских компонентов и ссылки на их JavaScript-файлы. * Пропсы, переданные от серверных компонентов к клиентским.
Клиент (браузер) получает этот Payload и "вклеивает" его в существующее дерево DOM. Это позволяет сохранять состояние клиентских компонентов (например, введенный текст в инпуте поиска) даже при смене маршрута, если этот инпут находится в общем лейауте.
Статический и динамический рендеринг
Next.js автоматически определяет, может ли страница быть статической.
* Static Rendering: Если страница не использует "динамические функции" (чтение кук, заголовков запроса или параметров поиска URL) и данные кэшируются, Next.js рендерит её один раз во время сборки.
* Dynamic Rendering: Если используется cookies(), headers() или fetch с параметром { cache: 'no-store' }, страница рендерится для каждого пользователя индивидуально в момент запроса.
Практический пример: Создание иерархии для интернет-магазина
Давайте спроектируем структуру каталога товаров, чтобы закрепить понимание иерархии.
app/layout.tsx): Здесь мы подключаем глобальные шрифты, провайдеры контекста и общую корзину.app/(shop)/layout.tsx): Добавляет навигацию по категориям и поисковую строку.app/(shop)/[category]/page.tsx):params.category.
* Делает запрос к базе данных: const products = await getProducts(params.category).
* Рендерит список карточек.
app/(shop)/[category]/[id]/page.tsx):loading.tsx для отображения скелетона, пока грузятся детали товара.
* Использует error.tsx для обработки случая, если товара с таким id не существует.В этом примере мы видим, как Next.js позволяет писать асинхронный код прямо в теле компонента (async function). Это возможно только в Server Components. Нам не нужно использовать useEffect и useState для загрузки данных, что избавляет нас от проблем с "Network Waterfalls" (каскадными запросами) на стороне клиента.
Оптимизация и производительность
Архитектура App Router спроектирована так, чтобы минимизировать объем работы, выполняемой в браузере. Однако разработчик должен следить за тем, где проходит граница между сервером и клиентом.
Правило «подъема» клиентских компонентов
Распространенная ошибка — помечать 'use client' весь лейаут или страницу целиком. Это превращает все дочерние компоненты в клиентские, даже если они не используют интерактивность. Правильный подход — спускать интерактивность как можно ниже по дереву компонентов.
Плохо:
* Page (Client) -> Header -> SearchInput -> StaticContent
* Весь StaticContent улетает в бандл клиента.
Хорошо:
* Page (Server) -> Header (Server) -> SearchInput (Client)
* Page (Server) -> StaticContent (Server)
Если вам нужно обернуть серверные компоненты в клиентский провайдер (например, для темы оформления или Redux), используйте паттерн children:
Этот нюанс критически важен для интеграции Redux Toolkit, которую мы будем подробно разбирать в следующих главах. Store провайдер должен быть клиентским компонентом, но он не должен превращать всё приложение в клиентское.
Взаимодействие с метаданными
Next.js предоставляет встроенный Metadata API для управления тегами <head>, такими как title и meta. Это заменяет сторонние библиотеки вроде react-helmet.
Метаданные могут быть статическими:
Или динамическими, зависящими от параметров страницы:
Next.js дождется выполнения generateMetadata перед тем, как начать стриминг HTML клиенту, что гарантирует правильное отображение превью в социальных сетях и поисковиках.
Замыкание архитектурной мысли
Переход на App Router — это не просто изучение новых имен файлов. Это переход к модели, где сервер является полноправным участником жизненного цикла компонентов. Мы научились структурировать приложение через папки, использовать специальные файлы для обработки состояний загрузки и ошибок, а также эффективно распределять код между сервером и клиентом.
Понимание того, как маршруты вложены друг в друга и как layout.tsx управляет иерархией, подготавливает нас к следующему шагу: управлению глобальным состоянием. В традиционных SPA Redux хранил всё: от данных пользователя до состояния открытого модального окна. В мире Next.js часть этой ответственности уходит на уровень серверных компонентов и кэша маршрутов. В следующей главе мы разберем, как спроектировать Redux Store так, чтобы он гармонично дополнял архитектуру Next.js, не вступая с ней в конфликт.