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

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.

Комментарии (1)


  1. NowebNolife
    00.00.0000 00:00
    +1

    Радует, что наконец-то начали работать над ошибками.
    Последние годы знатно горело так, что начал всё больше использовать Remix.

    На данный момент остро не хватает экспорта в SSG. Зная Vercel, чёрт знает, когда они его допилят.