Всех приветствую и желаю приятного чтения!

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:

Установка и использование новых экспериментальных возможностей

Для установки с использованием новых возможностей можно использовать 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.

online demo.

Файлы примера:

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

error.js и global-error.js

Используется для выделения ошибок в определенных частях приложения. Он автоматически помещает страницу или дочерний макет в  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".

Online demo

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 которое уже устарело.

Есть некоторые нерешенные проблемы, которые публиковал я:

Из приятных новостей:

  • Хорошо написанная документация с примерами на typescript.

  • Удобное использование клиентских и серверных компонентов в одном дереве компонентов.

  • Кэширование сегментов и запросов на клиенте и сервере.

  • Маршрутизация с использованием компонентов оберток делает код понятнее и проще.

  • Потоковая передача данных по HTTP с использованием React.Suspense.

Нейтральные нововведения для меня:

  • Использование нового формата метаданных и поддержка JSON-LD.

Спасибо Вам что дочитали до конца, надеюсь приятно провели время и получили полезную информацию!

Статья на моем сайте:

Пробую новые возможности Next.js 13. Часть 1.
Пробую новые возможности Next.js 13. Часть 2.