Всех приветствую и желаю приятного чтения!
Next.js это fullstack фреймворк разработанный Vercel использующий последние разработки React.
Не так давно 25 октября 2022 года вышла версия 13. На данный момент последняя стабильная версия 13.2.3, и новые возможности все еще находятся в стадии бета теста.
13 поддерживает все возможности версии 12. Для тестирования новых возможностей используется специальная директория app. Такой подход помогает попробовать новые возможности, в проектах, которые работали на версии 12.
В этой статье я пробую использовать только новые возможности версии 13, кому интересно больше узнать о Next.js рекомендую: Next.js: подробное руководство. Итерация первая.
Краткое содержание статьи
Описание разделов:
Серверные и клиентские компоненты
Серверные компоненты доступны стали доступны для использования. Рассмотрим особенности определения серверного и клиентского кода, задачи и возможности компонентов, использование одном дереве компонентов и серверные функции.
Выборка данных и кэширование
Добавлена новая функция выборки fetch c возможностью настройки кэширования, которая может использоваться на клиенте и сервере. Клиентскому маршрутизатору добавлено автоматическое кэширование сегментов при навигации. Серверный кэш сегментов.
Сегмент - это часть URL пути разделенная слешами.
Маршрутизация
Построена на работе с сегментами и новой файловой структурой. Основные темы:
Родительский сегмент содержит компоненты обертки над дочерними сегментами, они добавляют: обработку ошибок, состояние загрузки, слои, шаблоны и другие обертки, подробнее о которых будет в главе "Файлы сегмента маршрута".
Route groups - для организации сегментов, для того чтобы применить к ним одинаковые настройки, организовать сегменты в структуру, не влияя на структуру URL, создания нескольких корневых layout.
Динамические сегменты - для построения маршрутов из динамических данных, основан на использование квадратных скобок в именах файлов и директорий, не сильно отличается от того что используется в pages. Подробности в главе "Динамические сегменты".
Route Handlers - обработчики маршрута для построения своего API для обработки http запросов, альтернатива pages/api. Подробности в главе "Обработчики маршрута".
Потоковой передачи Http и компонент Suspense
Использование потоковой передачи Http в сочетании компонентом Suspense возможно для серверных и клиентских компонентов, находящихся в одном дереве компонентов. Подробности в "HTTP Streaming и Suspense"
Метаданные и SEO
Новый подход к добавлению метаданных на страницу c помощью объектов js и поддержка JSON-LD - это формат микроразметки описания контента с помощью объектов словаря связанных данных.
Немного заметок и выводы
Для каждого раздела есть пример кода…
Примеры кода
Все примеры хранятся в репозитории Github next13-app-exp и развернуты на Vercel, потому что там можно автоматически развернуть в продакшен каждую ветку.
Список примеров по названию веток:
Code router-dynamic / Online Demo - пример работы с динамической маршрутизации, и тест параметра сегмента dynamicParams управляющим динамической генерацией страниц после сборки. Пока есть проблема с подключением своего not-found.js и в этом обсуждении есть обходной путь.
Code context / Online demo - пример работы с контекстом в клиентских компонентах используется в главе "Работа с контекстом на стороне клиента".
Code server-fetch-standalone / Online demo- пример работы серверного и клиентского fetch с опцией revalidate: 60, с кэшем подробнее в главе "Выборка данных и кэширование". Пока опция revalidate: 60 не работает баг репорт
Code static-dynamic-segments / Online demo - пример использования статических и динамических сегментов в одном URL пути, в зависимости от того какие будут параметры последнего сегмента, так будет генерироваться весь путь.
Code suspense / Online Demo - демонстрация потоковой передачи данных. Нескольких серверных компонентов, делают выборку на стороне сервера, и загружаются в одном клиентском компоненте с использованием компонента Suspense не нарушая интерактивность страницы. Подробности в главе "Потоковая передача и компонент Suspense".
server-fetch-custom-cache, - делаем свой кэш для демонстрации работы с данными в серверных компонентах. Подробнее будет в главе "Передача данных между серверными компонентами".
Примеры, используемые в главе "Маршрутизация":
Code loading / Online Demo - пример работы файла loading.js, который добавляет обертку Suspense к сегменту
Code error-boundaries / Online Demo - пример работы файла error.js перехват ошибок клиентских и серверных компонентов.
Code templates / Online Demo - пример работы файла template.tsx, форма обратной связи одна для всех сегментов и перезагружается на каждый переход между сегментами, за исключением сегментов, объединенных с помощью Route Groups.
Code multiple-root-layouts / Online Demo - пример работы нескольких Root Layout, в этом примере нет корневого файла layout.js, вместо этого созданы две папке в каждой из которых есть layout.js Root Layout. Примечание: При переключении между Root Layout происходит полная перезагрузка страницы. В 13.1.6 было немного другое поведение, и я надеялся, что полной перезагрузки не будет. Обсуждение так было в 13.1.6 можно было перейти на другой root layout 3 раза без перезагрузки страницы.
Есть еще большая демка от Vercel для тестирования новых возможностей.
В github репозитории Next.js 13 в папке examples можно найти несколько примеров адаптированных для app:
app-dir-i18n-routing - многоязычный сайт, сделанный через множественный RootLayout
with-grafbase - работа с graphlq
Установка и использование новых экспериментальных возможностей
Для установки с использованием новых возможностей можно использовать create-next-app с опцией experimental-app
npx create-next-app@latest --experimental-app
Если хотите попробовать самые последние обновления, которые еще не вошли в основную ветку нужно установить версию canary вместо latest.
Включаем экспериментальное возможности, если установка была без experimental-app
next.config.js const nextConfig = { experimental: { appDir: true, }, }
После установки будет доступна папка app в которой можно тестировать новые возможности, папка pages также доступна в которой все работает также, как и в 12 версии. pages и app работают одновременно, в app также, как и в pages можно настраивать маршрутизацию и нужно следить за тем чтобы маршруты не пересекались. Одновременное использование папок app и pages дает возможность протестировать уже существующие проекты, частично используя нововведения из папки app.
В документации есть гайд по миграции приложения из папки pages в app.
Серверные и клиентские компоненты
Серверные компоненты в Next.js 12 были доступны в стадии (альфа), для того чтобы использовать серверные компоненты нужно было добавить слово server перед расширением файла "component.server.js". В 13 версии, используется другой подход к использованию серверных компонентов.
В каталоге app все компоненты по умолчанию являются серверными, также если компонент из app импортирует другой компоне��т вне каталога app он также будет по умолчанию серверным.
Чтобы обозначить что компонент является клиентским нужно в начале модуля компонента использовать директиву "use client", будет далее в примере показано как это сделать.
Дополнительно для того можно указать что код должен использоваться только на сервере с помощью server-only или клиенте client-only.
Серверными могут быть только функциональнее компоненты, но без возможности работать с состоянием и хуками, которым нужно состояние.
Если используем классовый компонент на сервере получим ошибку:
React Class Components only works in Client Components
Серверные и клиентские компоненты могут чередоваться в одном и том же дереве компонентов.
В случае если клиентский компонент родитель и серверный дочерний, нужно серверный компонент передавать через props children.
Серверные компоненты могут быть синхронными и асинхронными.
В текущий момент если дочерний асинхронный серверный компонент использовать с Typescript это приведет к ошибке
'ServerComponent' cannot be used as a JSX component. Its return type 'Promise<Element>' is not a valid JSX element. Type 'Promise<Element>' is missing the following properties from type 'ReactElement<any, any>': type, props, key
рекомендация от разработчиков использовать
{/* @ts-expect-error Server Component */}
в будущем это должно быть исправлено.
app/page.tsx
import { ServerComponent } from "@/components/ServerComponent"; import { ClientComponent } from "@/components/ClientComponent"; export default async function Page({ params }: { params: { page: string } }) { return ( <ClientComponent header={params.page}> {/* @ts-expect-error Server Component */} <ServerComponent page={params.page} /> </ClientComponent> ); }
components/ClientComponent.tsx
'use client'; export default function ClientComponent({children}) { return ( <> {children} </> ); }
components/ServerComponent.tsx
import { fetchData } from "@/lib/fetchData"; export const ServerComponent = async ({ page }: { page: string }) => { const {data} = await fetchData(page) return <>{data}</> }
Серверные компоненты рекомендуются использовать до тех пор, пока не будет необходимости в клиентских компонентах. Предполагаю это поможет уменьшить размер Client Side React кода, потому что render серверных компонентов будет выполнен на сервере, а в клиент будет отправлен готовый HTML CSS и JS для работы с API браузера.
Типичные задачи для серверных компонентов:
Выборка и кэширование запросов на стороне сервера с помощью новой функции fetch.
Хранения приватных данных для доступа к внешнему API.
Работа с серверным API Next.js и Node.js.
Хранение кода тяжелых зависимостей на сервере, чтобы уменьшить размер Client Side React кода.
и для клиентских компонентов:
Работа с хуками, работающими с состоянием React компонентов.
Работа с классовыми компонен��ами React.
Работа с событиями пользовательского интерфейса.
Работа с браузерным API.
Полный список серверных функций
cookies - считывать cookie входящего запроса HTTP.
fetch - делает выборку данных
headers - считывает заголовки запроса HTTP.
generateStaticParams - определяет список параметров сегмента маршрута, которые будут статически генерироваться во время сборки.
notFound - принудительно вызывает компонент из файла not-found.js и добавляет мета тэг name="robots" content="noindex"
redirect - перенаправляет клиента на другой URL
NextRequest и NextResponse - используются в Route handler, подробнее в главе "Обработчики маршрута"
Выборка данных и кэширование
В app доступно кэширование выборки fetch и сегментов на клиенте и сервере.
Клиентский и Серверный кэш на уровне выборки данных с помощью функции fetch
fetch это одна из новых функций Next.js 13 прототипом который была функция fetch Web Api.
Fetch может быть использована на клиенте и браузере.
С помощью второго параметра функции можно управлять кэшированием:
{cache: "no-store"} - не кэшировать - {cache: "force-cache"} (default) - кэшировать
{next: { revalidate: number } } - хранить кэш определенное время
online demo - демонстрирует как работает кэширование fetch с опцией хранить кэш 60 секунд:
const response = await fetch(url, { next: { revalidate: 60 }, });
В демо функция fetch запускается из серверного и клиентского компонента. На стартовой странице нужно выбрать id, запроса, кэш которого будем тестировать, далее с помощью радио кнопок выбрать серверный или клиентский fetch:

Пока что в браузере (Chrome | Firefox) опция revalidate не работает, на сервере работает отлично.
Обсуждение почему не работает revalidate в браузере
Опция cache работает в браузере и на сервере. Код примера для теста. Код этого примера нужно запускать оффлайн, я не придумал онлайн пример чтобы было понятно показать, как работает постоянный кэш на сервере. В демке используется json-server, который очень прост в настройке, смотри readme репозитория). Json-server запущенный из командной строки отображает каждый запрос, который он обработал. Если в демо запрашивать одни и те же данные с включенным постоянным кэшированием повторных с одинаковыми параметрами запросов к json-server не будет.
Не запрещено пользоваться другими утилитами для выборки данных, в этом случае кэширование будет зависеть от возможностей этих утилит. Например, axios или SWR хорошо подойдут для кэширования запросов.
На заметку:
Вместо имени хоста "localhost" в запросах серверного fetch, лучше использовать 127.0.0.1 иначе можно получить такую ошибку:
{"cause":{"errno":-4078,"code":"ECONNREFUSED","syscall":"connect","address":"::1","port":3000}}
которая случается не каждый раз при использовании localhost, т.е. может и не случиться. Точной причины и периодичности я не выявил, есть подозрения что она появляется только в Node.js 18 при не выявленных условиях.
У fetch были проблемы с кэшем на стороне сервера до версии 13.2.
Клиентский кэш на уровне сегментов
Новый маршрутизатор имеет кэш на стороне клиента в памяти in-memory client-side cache, в котором сохраняется результат визуализации (render result) серверных компонентов по мере того, как пользователь перемещается по приложению.
Кэш можно аннулировать с помощью router.refresh().
Серверный кэш на уровне сегментов
Это SSG и SSR который также был и в pages версии 12. В pages работа была со страницами, в app с сегментами. Сегмент в отличии от страницы, представляет собой часть URL пути разделенный "/".
В app свое API, но работает очень похоже на pages:
сегмент может быть статическим динамическим, сгенерирован по требованию
сегменты можно сгенерировать во время сборки, а для тех, которые не сгенерированы задать поведение как они будут обрабатываться, будет страница 404 или они будут генерироваться и запоминаться на определенное время или навсегда.
Для настройки кеширования используются параметры, параметры экспортируются из специально именованных файлов, находящихся в папке сегменте, подробнее о файлах маршрута в следующей главе "Маршрутизация".
export const dynamic = 'auto' export const dynamicParams = true export const revalidate = false
Дополнительную информацию о опциях можно найти в разделе документации Route Segment Config.
Рассмотрим работу кэширования сегментов на примере.
Структура файлов демки:
app │ globals.css │ layout.module.scss │ layout.tsx │ page.tsx │ └───static │ layout.tsx │ page.tsx │ └───dynamic page.tsx
Маршруты строятся с помощью вложенности папок.
В демо используется путь /static/dynamic, состоящий из двух сегментов, идущих друг за другом:
static - этот сегмент кэшируется как статический, потому что в файле static\layout.tsx опция dynamic=force-static и она будет действовать на маршрут /static.
dynamic - этот сегмент динамически и не кэшируется, потому что в файле static\dynamic\page.tsx указана опция dynamic=force-dynamic, и она будет действовать на маршрут /static/dynamic.
Кэш маршрута /static хранится отдельно, т.е. маршрут /static/dynamic не перепишет кэш /static.
Эта демка демонстрирует одну особенность. Чтобы ее понять, нужно знать, что такое "слои" они подробнее в следующей главе "Маршрутизация". Слой - это компонент который обертывает текущий сегмент и дочерние сегменты, и хранит свое состояние при навигации по дочерним сегментам. В демке слой для static сегмента выводит время своей генерации на сервере. И по идее если сегмент "force-static" он не должен перерисовываться, это так и работает пока мы на сегменте статик.
Если перейти со static сегмент на dynamic сегмент, то у видим, что компонент layout сегмента static отобразился с данными из кэша. В документации Partial Rendering написано, что должны перерисовываться только дочерние сегменты, при навигации, т.е. работает все верно, как по документации.

Что у меня вызвало вопрос это, если на dynamic сегменте нажать кнопку "Refresh current segment", которая запускает router.refresh(), для очистки клиентского кэша и запроса новых данных с сервера, layout который пришел из static сегмента получит обновленные данные с сервера, не смотря на то что он force-static:

На кэш сегмента /static это не влияет. Интересно что генерация кода выполняется на сервере, я это узнал просто, добавив в компонент console.log("generation"), собрал и запустил сервер, и на каждое нажатие кнопки "Refresh current segment" в логе сервера видел это сообщение.
Если перейти на сегмент dynamic нажать "Refresh current segment" перейти на сегмент home и вернуться на dynamic, поведение будет такое же, как и при нажатии кнопки "Refresh current segment".
Пока не понятно это баг или фича, обсуждаем тут.
Маршрутизация
Новая маршрутизация Next.js 13 построена работе с сегментами. Сегмент представляет собой часть URL пути разделенный "/".

.
Сегмент представляет собой набор специально именованных файлов js расположенных в одной папке, каждый файл содержит серверные или клиентские компоненты, обработчики состояний загрузки , ошибок (Error boundaries может быть только клиентским), страницы 404. Папки сегментов могут быть вложены.
Вложенность сегментов - это новая возможность,

создав в родительском сегменте компонент слой (layout.js), этот компонент будет оберткой для все дочерних сегментов, в которых могут быть свои слои. Эта возможность работает и для других компонентов оберток: шаблонов, загрузчиков, обработчиков ошибок. Сравнивая с реализацией вложенности слоев в pages, новый подход на основе компонентов оберток app это упрощает и уменьшает написание кода.
Файлы сегмента маршрута
Документация содержит хорошее описание компонентов в файлах сегмента на Typescript.
В папке app можно хранить любые файлы, главное, чтобы имена не совпадали со спец. файлами.
Файлы из pages _app и _document в app заменены функционалом файлов layout и page.
Специально именованные файлы маршрутизации в папке app генерируют дерево компонентов со следующей иерархией:

Краткое описание файлов:
page.js: создает уникальный UI и делает маршрут доступным
route.js: добавляет Route Handlers для обработки запросов HTTP (server-side API endpoints).
layout.js: Создайте общий пользовательский интерфейс для сегмента и его дочерних элементов. Макет оборачивает страницу или дочерний сегмент.
template.js: Похожий на layout.js , за исключением того, что новый экземпляр компонента монтируется и размонтируется при навигации по дочерним сегментам.
loading.js: Обертывает страницу или дочерний сегмент в компонент React Suspense.
error.js: Обертывает страницу или дочерний сегмент в компонент React Error Boundary
global-error.js: Похожий на error.js, но ловит ошибки только в корневом layout.js.
not-found.js: rКомпонент в этом файле будет будет использоваться когда будет вызов notFound
Рассмотрим каждый из этих файлов подробнее.
page.js
Используется для определения уникального пользовательского интерфейса на конце маршрута.
Примечания:
Если файл page отсутствует в сегменте будет отображена страница 404 по этому маршруту.
Page может использоваться для добавления метаданных и статической генерации страниц во время сборки используя generateStaticParams.
В сегменте может быть либо page либо route.js, но не оба сразу.
[slug]\page.tsx
export type TProps = { params: { slug: string }; searchParams?: { [key: string]: string | string[] | undefined }; }; export default function Page(props: TProps) { return <PageComponent {...props} />; }
Props:
params - имя сегмента или сегментов если используется динамическая маршрутизация
searchParams - параметры поиска
layout.js
Обязательный файл, используемый для определения пользовательского интерфейса, который является общим для всех дочерних сегментов.
Примечания:
Обязательно должен быть хотя бы один RootLayout
Корневой файл layout.js это лучшее место для использования функций инициализации и подключения глобальных контекстов ( подключение библиотек управления состоянием, контексты графических фреймворков ) на стороне клиента.
RootLayout может быть не один, смотри пример Example: Creating multiple root layouts. При переходе между RootLayout происходит полная перезагрузка страницы, что для меня было немного [неожиданно]. Смотри демку и я не понял почему они сделали полную перезагрузку так как в 13.1.6 работало почти без перезагрузки, но в [13.1.7] перезагрузка после каждого перехода. Этот вариант хорошо подходит чтобы сделать много язычный сайт.
layout используется для добавления метаданных и использования тегов script и link, так head.js в 13.2 устаревает.
Интересный факт, не знаю упомянут ли он в доке, если в папке app не будет ни одного файла RootLayout, то
в логе отладочного сервера получим сообщение:
Your page app/page.tsx did not have a root layout. We created app\layout.tsx for you.
layut.js восстановлен с таким содержимым:
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html> <head /> <body>{children}</body> </html> ) }
С другими файлами я такого поведения не заметил.
Props:
children - компонент page этого или дочернего сегмента со всеми обертками согласно этой иерархии
params - имя сегмента или сегментов если используется динамическая маршрутизация
route.js
В версии 13.1.7-canary.23 добавлен новый инструмент для создания API, который получил название Route Handlers, замена API Routes в папке pages. Сейчас он доступен начиная с 13.2 в основной версии, а не только в canary. Подробнее будет в главе "Обработчики маршрута"
Экспорт значений из файлов: layout, page, route
Layout, page, route могут экспортировать настройки на уровне сегмента. Подробнее в доке:
dynamic - можно принудительно сделать компонент динамическим/ Допустимые значения: 'auto'(default) | 'force-dynamic' | 'error' | 'force-static'. По умолчанию сегмент кэшируется во время сборки и будет статическим, это означает . Смотри приложение static vs dynamic fallback
dynamicParams - эта опция заменяет параметр fallback из getStaticPaths Next.js 12,
revalidate - false или число - эта опция заменяет параметр revalidate из getStaticProps Next.js 12,
fetchCache - указывает как будет работать с кэшем специальная серверная функция fetch,
runtime - выбор между edge и nodejs runtimes,
preferredRegion - в случае использования нескольких серверов можно настроить выборку данных по регионам, что сокращает задержку и повышает производительность. Setting Serverless Function Regions Подробнее.
Дополнительно layout и page экспортируют метадату. Подробнее в главе "SEO и метаданные"
template.js
Templates похожи на layouts тем, что они обертывают свой и дочерние сегменты, но основная разница в том что при каждой навигации по дочерним сегментам, создается новый экземпляр template, за исключением маршрутов, которые находятся в одной Route Groups.
Файлы примера:
app │ layout.tsx │ page.tsx │ template.tsx │ ├───(marketing) │ ├───about │ │ page.tsx │ │ │ └───blog │ page.tsx │ └───(shop) └───account page.tsx
Видно, что файл template.tsx есть только в корневой папке, компонент template оборачивает дочерние сегменты, компонент содержит форму обратной связи для демонстрации работы пересоздания компонента при навигации по сегментам.
Состояние по умолчанию формы "готова к отправке", если нажать "отправить" состояние формы перейдет в состояние "отправлено", и состояние сбросится перейти на другой сегмент, так как создастся новый экземпляр формы с состоянием по умолчанию.
Сегменты About и Blog, расположены в Route Groups это папка marketing с круглыми скобками.
Route Groups не оказывают влияния на формирование сегментов пути, т.е. не добавляется новый сегмент пути маршрута, например, marketing в путь URL Route Groups и служат для группировки маршрутов.
При навигации внутри "Route Groups" компонент template не пересоздается. Т.е. если мы перейдем на сегмент About нажмем отправить, а затем перейдем на сегмент Blog не произойдет создания нового экземпляра формы обратной связи.
На всякий случай опрос баг или фича.
Template может быть использован для:
Использование stateless компонентов, которые при навигации пересоздаваться запуская css/js анимацию.
Подключения компонентов, которые требуют инициализации при переходе на каждый сегмент, например форма обратного отзыва на каждую страницу. как показано в демо
Обертка Suspense внутри layout будет показывать fallback один раз, внутри template fallback будет показываться каждый раз.
Props:
• children - компонент page этого или дочернего сегмента со всеми обертками согласно этой иерархии
loading.js
Используется для создания пользовательского интерфейса загрузки для определенной части приложения. Он автоматически помещает страницу или дочерний макет в "одну" обертку React Suspense. По умолчанию все дерево внутри "каждого" Suspense рассматривается как единое целое, все они вместе будут заменены индикатором загрузки, определённым в loading.js. демо для loading.js.
export default function Loading() { return <LoadingSkeleton /> }
NoProps
Используется для выделения ошибок в определенных частях приложения. Он автоматически помещает страницу или дочерний макет в React Error Boundary. Компонент обработчик ошибки должен быть клиентским. online demo обработки ошибок на стороне сервера и клиента
Props:
• error - экземпляр объекта Error
• reset - функция для сброса Error Boundary.
head.js
В 13.2 этого файла уже не будет, метадата будет формироваться в файлах laout и page
Используется для наполнения тега head. Обычно находится корневой папке app, но в случае с несколькими RootLayout может находится в каждой папке RootLayout. в head.js можно было использовать теги style и script, теперь они судя по всему будут подключаться в layout.js.
Так выглядел head.js:
export default function Head() { return ( <> <title>Create Next App</title> <meta content="width=device-width, initial-scale=1" name="viewport" /> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </> ) }
так выглядит новая metadata в layout.js
export const metadata = { title: "Create Next App", description: "Generated by create next app", icons: { icon: "/favicon.ico", }, };
not-found.js
Файл используется если будет вызвана функция notFound, пока not-found.js не вызывается автоматически, если маршрут не найден будет вызвана страница 404 не из файла "not-found.js", а по умолчанию. Надеюсь это поведение поменяется, в баг репорте мне ответили "We are working on that".
NoProps
Динамические сегменты
Динамический сегмент можно создать, заключив имя папки, в квадратные скобки, например: [id] или [slug].
Динамическое имя сегмента можно получить в page.js, layout.js, route.js.
Простой пример
import { Blog as BlogComponent } from "@/components/blog"; export type TProps = { params: { slug: string }; searchParams?: { [key: string]: string | string[] | undefined }; }; interface IPage { (props: TProps): JSX.Element; } export default function Blog(props: Tprops) { console.log(props.params); return <BlogComponent {...props} />; }
Route | Example URL | params |
|---|---|---|
app/blog/[slug]/page.js | /blog/a | { slug: 'a' } |
app/blog/[slug]/page.js | /blog/a | { slug: 'a' } |
app/blog/[slug]/page.js | /blog/b | { slug: 'b' } |
app/blog/[slug]/page.js | /blog/c | { slug: 'c' } |
С помощью параметра dynamicParams в файлах layout.js / page.js / route.js
export const dynamicParams = true | false;
можно разрешить или запретить генерировать сегменты, кроме тех что возвращает функция generateStaticParams. В page.js - эта опция аналог опции fallback из getStaticPaths, которая используется в pages. online demo
Пример использования generateStaticParams в демке
[slug]\page.tsx
import { Page as PageComponent} from "@/components/page"; export async function generateStaticParams() { return [{slug: "1"}, {slug: "2"}]; } export type TProps = { params: { slug: string }; searchParams?: { [key: string]: string | string[] | undefined }; }; interface IPage { (props: TProps): JSX.Element; } export default function Page(props: Tprops) { return <PageComponent {...props} />; } export const dynamicParams = false;
catch-all
Перехват имен всех дочерних сегментов [...slug] возможно добавив многоточие внутри скобок
Route | Example URL | params |
|---|---|---|
app/shop/[...slug]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[...slug]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[...slug]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
optional catch-all
Разница между сегментами catch-all и optional catch-all заключается в том, что при использовании optional также сопоставляется маршрут без параметра (/shop в примере выше).
Route | Example URL | params |
|---|---|---|
app/shop/[[...slug]]/page.js | /shop | {} |
app/shop/[[...slug]]/page.js | /shop/a | { slug: ['a'] } |
app/shop/[[...slug]]/page.js | /shop/a/b | { slug: ['a', 'b'] } |
app/shop/[[...slug]]/page.js | /shop/a/b/c | { slug: ['a', 'b', 'c'] } |
Если нужно использовать generateStaticParams для catch-all и optional catch-all нужно возвращать значение slug как массив:
export async function generateStaticParams() { return [{slug: ["1"]}, {slug: ["2"]}]; }
Обработчики маршрута
В папке app есть свой инструмент для обработки http запросов, который получил название Route Handlers, пришел на смену API Routes в папке pages. Обработчики запросов используются в файле route.js
Поддерживаемые методы: GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS
Обработчики запросов позволяют создать API для обработки запросов с использованием Request и Response, а также обернутые в типы next.js сервера NextRequest и NextResponse, которые добавляют работу с cookie и обертку nextUrl для URL.
export async function GET(request: Request) { const res = await fetch(url); const data = await res.json(); return Response.json({ data }) }
Обработчики запросов поддерживают возможности "динамических сегментов" и настройки Route Segment Config Options, для этого нужно переименовать папку содержащую маршрут с файлом route.js в соответствии с правилами динамических сегментов, а в компоненте обработчике запросов использовать второй аргумент функции для получения данных:
api[…slug]\route.js
import { NextResponse, type NextRequest } from "next/server"; export async function generateStaticParams() { return [{slug: "1"}, {slug: "2"}]; } export async function GET( request: NextRequest, { params }: { params: { slug: [string] } } ) { return NextResponse.json({ slug: params }); // slug: "1" or slug: "2" } export const dynamicParams = false;
в этом случае будут доступны только: /api/1 и /api/2 для остальных маршрутов 404.
В обработчиках запросов могут использоваться серверные функции из Next.js API.
Могут быть статическими и динамическими, для них так же действуют настройки Route Segment.
online demo - подробнее демо разобрано в главе "Выборка данных и кэширование". В этом демо используется обработчик Get запросов
import { NextResponse, type NextRequest } from "next/server"; export async function GET( request: NextRequest, { params }: { params: { id: string } } ) { let payload = Date.now(); if (params.id === "gettimezoneoffset") payload = new Date().getTimezoneOffset(); return NextResponse.json({ id: params.id, payload }); } export const dynamic = "force-dynamic";
Планы на будущее
В будущем, в Next.js Маршрутизатор предоставит набор соглашений, которые помогут вам реализовать более продвинутый шаблон маршрутизации. К ним относятся:
Параллельные маршруты: позволяют одновременно отображать две или более страниц в одном представлении, по которым можно перемещаться независимо.
Перехват маршрутов: позволяет перехватывать маршрут и показывать его в контексте другого маршрута. Вы можете использовать их, когда важно сохранить контекст для текущей страницы. Предполагаю, что это отображение двух page.js одновременно, дочернего в контексте родительского.
Условные маршруты: позволяют вам условно отображать маршрут на основе условия. Например, показывать страницу только в том случае, если пользователь вошел в систему.
Потоковая передача и компонент Suspense
При потоковой передаче HTTP сервер настроен на удержание определенного запроса от клиента и сохранение ответа открытым, чтобы он мог передавать через него данные. Клиент может прослушивать обновления с сервера и получать их мгновенно без каких-либо накладных расходов, связанных с HTTP-заголовками и открытием/закрытием соединений.
В сочетании с клиентскими компонентами и Suspense, серверные компоненты React могут передавать контент через потоковую передачу по HTTP.
Потоковая передача хорошо работает с компонентной моделью React, потому что каждый компонент можно рассматривать как фрагмент (chunk). Это позволяет отображать части страницы раньше, не дожидаясь загрузки всех данных, прежде чем можно будет отрисовывать какой-либо пользовательский интерфейс.
В Next.js можете реализовать потоковую передачу используя loading.js, для всего сегмента маршрута, или с Suspense, для более детального контроля.
Полный код примера, demo online
import { Suspense } from "react"; import { Spinner } from "@/components/Spinner"; import { ServerComponent } from "@/components/ServerComponent"; import { ClientComponent } from "@/components/ClientComponent"; export default async function Page({ params }: { params: { id: string } }) { return ( <ClientComponent id={params.id}> <Suspense fallback={<Spinner />}> {/* @ts-expect-error Server Component */} <ServerComponent delay={1} /> </Suspense> <Suspense fallback={<Spinner />}> {/* @ts-expect-error Server Component */} <ServerComponent delay={2} /> </Suspense> <Suspense fallback={<Spinner />}> {/* @ts-expect-error Server Component */} <ServerComponent delay={3} /> </Suspense> </ClientComponent> ); } export const dynamic = "force-dynamic";
В этом примере демонстрируется как с помощью Suspense блоков можно разбить на фрагменты загружаемый контент.
SEO и метаданные
Next.js поддерживает описание метаданных с помощью тега meta и JSON-LD это формат микроразметки описания контента с помощью объектов, коллекция взаимосвязанных наборов данных в WEB. Эти данные могут быть экспортированы из layout.js и page.js. Метаданные могут быть размещены только в серверных компонентах.
Метаданных в тегах meta
До 13.2 метаданные размещались в файле head.js это был типичный html формат.
export default function Head() { return ( <> <title>Create Next App</title> <meta content="width=device-width, initial-scale=1" name="viewport" /> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/favicon.ico" /> </> ) }
Начиная с 13.2 новый формат это статические экспортируемый объект с именем metadata или динамический созданный с помощью generateMetadata.
Пример (Code repository , sandbox, deploy) добавления метаданных индивидуальный для каждого сегмента:
app\services\page.tsx
import { metaTags } from "@/data"; export const metadata = { title: metaTags.services.title, description: metaTags.services.description, keywords: metaTags.services.keywords, icons: { icon: "/favicon.ico", }, }; export default async function Page (){ return <>Service page</> }
app\solutions\page.tsx
import { metaTags } from "@/data"; export const metadata = { title: metaTags.solutions.title, description: metaTags.solutions.description, keywords: metaTags.solutions.keywords, icons: { icon: "/favicon.ico", }, }; export default async function Page (){ return <>Solutions page</> }
JSON-LD
JSON-LD — это формат микроразметки описания контента с помощью объектов словаря связанных данных. JSON-LD поддерживается в Yandex и Google
Пример использования
export default async function Page({ params }) { const product = await getProduct(params.id); const jsonLd = { "@context": "http://schema.org", "@type": "FlightReservation", reservationId: "RXJ34P", }; return ( <section> {/* Add JSON-LD to your page */} <script type="application/ld+json">{JSON.stringify(jsonLd)}</script> {/* ... */} </section> ); }
В примере показаны три ключа:
@context (зарезервированный) — указывает на то, что в объекте используется словарь Schema.org.
@type (зарезервированный) — указывает на тип FlightReservation, в свойствах которого можно указать данные о бронировании билета на авиарейс.
reservationId — соответствует свойству reservationId типа FlightReservation и содержит номер бронирования билета.
Заметки
Вызов функций в JSX клиентских компонентов
С виду безвредный код
<div> {moment(value).format("MMMM Do YYYY, h:mm:ss a")} </div>
дает предупреждение
Text content did not match. Server: "February 27th 2023, 10:44:57 pm" Client: "February 27th 2023, 10:44:59 pm"
Решение создать клиентский компонент, похожее решение предлагалось для библиотек компонентов, не адоптированных к использованию "use client"
"use client" const ClientMoment = ({ val }: { val?: string }) => { const [valDate, setValDate] = React.useState<string>(); React.useEffect(() => { setValDate(moment(val).format("MMMM Do YYYY, h:mm:ss a")); }, [val]); return <div>{valDate}</div>; };
и вносить изменения именно через useEffect, если написать просто
const [valDate, setValDate] = React.useState(moment(val).format("MMMM Do YYYY, h:mm:ss a"));
предупреждение продолжит появляться
Работа с контекстом в клиентских компонентах
React Context - Контекст позволяет передавать данные через дерево компонентов без необходимости передавать пропсы на промежуточных уровнях. Дока по использованию контекста в app.
подключаем контекст в layout.js и используем в каждом из сегментов about, blog и shop.
│ ClientContext.tsx │ globals.css │ layout.module.scss │ layout.tsx │ page.tsx │ ├───(marketing) │ ├───about │ │ page.tsx │ │ │ └───blog │ page.tsx │ └───(shop) └───account page.tsx
файл app\ClientContext.tsx - создаем контекст и клиентский компонент, который будем подключать в дерево серверных компонентов в файле layout.js.
"use client"; import React from "react"; interface IContexte { id: string; setId: (id: string) => void; } export const Context = React.createContext<IContexte | null>(null); export function ClientContext({ children }: { children: React.ReactNode }) { const [id, setId] = React.useState(""); return <Context.Provider value={{ id, setId }}>{children}</Context.Provider>; }
Подключаем контекст в layout.js
export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( … <main className={styles.main}> <ClientContext>{children}</ClientContext> </main> … ); }
в page.js добавляем клиентский компонент для использования контекста, далее работаем через useContext как обычно.
app\page.js
import { Page } from "@/components/Page"; export default function Home() { return <Page headerText="Home"/>; }
components\Page.tsx
"use client"; import styles from "./Page.module.scss"; import React from "react"; import { Context } from "@/app/ClientContext"; export const Page = ({ headerText }: { headerText: string; }) => { const context = React.useContext(Context); const [input, setInput] = React.useState(context?.id as string); const handlerSetId = () => { context?.setId(input); }; return ( <section className={styles.section}> <h2 className={styles.header}>{headerText}</h2> <div>Current id: {context?.id} </div> <div className={styles.inputGroup}> <input type="text" onChange={(e) => setInput(e.target.value)} value={input} className={styles.input} /> <button onClick={handlerSetId} className={styles.button}> setId </button> </div> </section> ); };
Контекст работает так, как и ожидалось никаких проблем не замечено.
Передача данных между серверными компонентами
Серверные компоненты не работают с сосанием и контекстом, для передачи данных между компонентами рекомендуется использовать:
кэш операций функций таких как fetch, т.е. вызывая функцию с одинаковыми параметрами мы должны получать одинаковый результат, или разный в зависимости от того устарел ли кэш. В любом случае этот результат будет релевантным. Тут подойдет пример, который использовался в главе "выборка данных и кэширование" server-fetch-standalone, если переключатель radiobutton установить на работу с серверной функцией fetch, так как параметр revalidate пока не работает в браузере.
собственные шаблоны JavaScript, такие как глобальные синглтоны, в пределах области действия модуля, если у вас есть общие данные, к которым необходимо получить доступ нескольким серверным компонентам. Этот пример разработаем в этой главе.
Пример передачи данных через модули es6 и собственный кэш.
Код использования серверной функции fetch
import "server-only"; interface IfetchData { (id: string): Promise<string>; } type TCache = { [key: string]: string; }; const cache: TCache = {}; export const fetchData: IfetchData = (id) => new Promise(async (resolve) => { let data = ""; try { if (cache[id]) { data = cache[id]; } else { const response = await fetch("http://localhost:3001/users/" + id, { cache: "no-store", }); data = JSON.stringify(await response.json()); cache[id] = data; } } catch (e) { if (typeof e === "string") { data = `Error: ${e.toUpperCase()} `; } else if (e instanceof Error) { data = `Error: ${e.message}`; } } resolve(data); });
в качестве хранилища кэша используется переменная cache. Функция в серверном компоненте fetch в этом примере вызываете с параметрами не использовать кэш ( cache: "no-store" ).
Для того чтобы протестировать как работает серверная функция fetch я использовал json-server и генератор json mockaroo
db.json - база данных для json-server
запуск json-server
json-server --watch ./db.json -p 3001
во время работы сервера ведется лог запросов
GET /users/1 200 45.345 ms - 157 GET /users/2 200 27.988 ms - 155 GET /users/3 200 20.497 ms - 155
Выводы
Сейчас все еще ведется активная разработка беты версии добавляются новые возможности.
Последние из недавно добавленных в версии 13.2 это Route Handlers, есть Api которое уже устарело.
Есть некоторые нерешенные проблемы, которые публиковал я:
Свой компонент для страницы 404 может быть вызван только с помощью функции notFound
Next.js fetch не работает с опцией revalidate - эту проблему можно обойти использовав, свой клиент для выборки с кэшированием
Из приятных новостей:
Хорошо написанная документация с примерами на typescript.
Удобное использование клиентских и серверных компонентов в одном дереве компонентов.
Кэширование сегментов и запросов на клиенте и сервере.
Маршрутизация с использованием компонентов оберток делает код понятнее и проще.
Потоковая передача данных по HTTP с использованием React.Suspense.
Нейтральные нововведения для меня:
Использование нового формата метаданных и поддержка JSON-LD.
Спасибо Вам что дочитали до конца, надеюсь приятно провели время и получили полезную информацию!
Статья на моем сайте:
Пробую новые возможности Next.js 13. Часть 1.
Пробую новые возможности Next.js 13. Часть 2.
