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

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

Первая часть статьи

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

1
npx create-next-app@latest --experimental-app

Если хотите попробовать самые последние обновления, которые еще не вошли в основную ветку нужно установить версию canary вместо latest.

Включаем экспериментальное возможности, если установка была без experimental-app

1
2
3
4
5
6
next.config.js
const nextConfig = {
  experimental: {
    appDir: true,
  },
}

После установки будет доступна папка app в которой можно тестировать новые возможности, папка pages также доступна в которой все работает также, как и в 12 версии. pages и app работают одновременно, в app также, как и в pages можно настраивать маршрутизацию и нужно следить за тем чтобы маршруты не пересекались. Одновременное использование папок app и pages дает возможность протестировать уже существующие проекты, частично используя нововведения из папки app.

В документации есть гайд по миграции приложения из папки pages в app.

Потоковая передача и компонент Suspense

При потоковой передаче HTTP сервер настроен на удержание определенного запроса от клиента и сохранение ответа открытым, чтобы он мог передавать через него данные. Клиент может прослушивать обновления с сервера и получать их мгновенно без каких-либо накладных расходов, связанных с HTTP-заголовками и открытием/закрытием соединений.

В сочетании с клиентскими компонентами и Suspense, серверные компоненты React могут передавать контент через потоковую передачу по HTTP.

Потоковая передача хорошо работает с компонентной моделью React, потому что каждый компонент можно рассматривать как фрагмент (chunk). Это позволяет отображать части страницы раньше, не дожидаясь загрузки всех данных, прежде чем можно будет отрисовывать какой-либо пользовательский интерфейс.

В Next.js можете реализовать потоковую передачу используя loading.js, для всего сегмента маршрута, или с Suspense, для более детального контроля.

Полный код примера, demo online

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 SEO & Metadata поддерживает описание метаданных с помощью тега meta и JSON-LD это формат микроразметки описания контента с помощью объектов, коллекция взаимосвязанных наборов данных в WEB. Эти данные могут быть экспортированы из layout.js и page.js. Метаданные могут быть размещены только в серверных компонентах.

Метаданных в тегах meta

До 13.2 метаданные размещались в файле head.js это был типичный html формат.

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

Пример использования

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 клиентских компонентов

С виду безвредный код

1
2
3
          <div>
            {moment(value).format("MMMM Do YYYY, h:mm:ss a")}
          </div>

дает предупреждение

1
 Text content did not match. Server: "February 27th 2023, 10:44:57 pm" Client: "February 27th 2023, 10:44:59 pm"

Решение создать клиентский компонент, похожее решение ]предлагалось для библиотек компонентов, не адоптированных к использованию "use client"](https://beta.nextjs.org/docs/rendering/server-and-client-components#convention)

1
2
3
4
5
6
7
8
9
"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, если написать просто

1
const [valDate, setValDate] = React.useState(moment(val).format("MMMM Do YYYY, h:mm:ss a"));

предупреждение продолжит появляться

Работа с контекстом в клиентских компонентах

React Context - Контекст позволяет передавать данные через дерево компонентов без необходимости передавать пропсы на промежуточных уровнях. Дока по использованию контекста в app.

Демка и код

  • подключаем контекст в layout.js и используем в каждом из сегментов about, blog и shop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
│   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.

1
2
3
4
5
6
7
8
9
10
11
12
13
"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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (<main className={styles.main}>
          <ClientContext>{children}</ClientContext>
        </main>);
}

в page.js добавляем клиентский компонент для использования контекста, далее работаем через useContext как обычно.

app\page.js

1
2
3
4
5
import { Page } from "@/components/Page";

export default function Home() {
  return <Page headerText="Home"/>;
}

components\Page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

1
json-server --watch ./db.json -p 3001

во время работы сервера ведется лог запросов

1
2
3
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.

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

Автор сайта Денис aka mr_dramm