Как перейти с Pages Router на App Router

Это руководство поможет вам:

Обновление

Версия Node.js

Минимальная версия Node.js теперь v18.17. Подробнее см. в документации Node.js.

Версия Next.js

Для обновления до Next.js версии 13 выполните следующую команду с помощью предпочитаемого менеджера пакетов:

Терминал
npm install next@latest react@latest react-dom@latest

Версия ESLint

Если вы используете ESLint, обновите его версию:

Терминал
npm install -D eslint-config-next@latest

Полезно знать: Возможно, потребуется перезапустить сервер ESLint в VS Code, чтобы изменения вступили в силу. Откройте палитру команд (cmd+shift+p на Mac; ctrl+shift+p на Windows) и найдите ESLint: Restart ESLint Server.

Следующие шаги

После обновления ознакомьтесь со следующими разделами:

Обновление новых функций

Next.js 13 представил новый App Router с новыми функциями и соглашениями. Новый роутер доступен в директории app и сосуществует с директорией pages.

Обновление до Next.js 13 не требует использования App Router. Вы можете продолжать использовать pages с новыми функциями, работающими в обеих директориях, такими как обновлённый компонент Image, компонент Link, компонент Script и оптимизация шрифтов.

Компонент <Image/>

Next.js 12 представил улучшения для компонента Image с временным импортом: next/future/image. Эти улучшения включали меньше клиентского JavaScript, более простые способы стилизации изображений, лучшую доступность и нативную ленивую загрузку в браузере.

В версии 13 это поведение стало стандартным для next/image.

Доступны два кодмода для миграции на новый компонент Image:

  • Кодмод next-image-to-legacy-image: Безопасно и автоматически переименовывает импорты next/image в next/legacy/image. Существующие компоненты сохранят прежнее поведение.
  • Кодмод next-image-experimental: Добавляет инлайновые стили и удаляет неиспользуемые пропсы. Это изменит поведение существующих компонентов в соответствии с новыми стандартами. Перед использованием этого кодмода необходимо выполнить next-image-to-legacy-image.

Компонент <Link> больше не требует ручного добавления тега <a> в качестве дочернего элемента. Это поведение было добавлено как экспериментальная опция в версии 12.2 и теперь стало стандартным. В Next.js 13 <Link> всегда рендерит <a> и позволяет передавать пропсы в этот тег.

Например:

import Link from 'next/link'

// Next.js 12: `<a>` должен быть вложенным, иначе он исключается
<Link href="/about">
  <a>About</a>
</Link>

// Next.js 13: `<Link>` всегда рендерит `<a>` под капотом
<Link href="/about">
  About
</Link>

Для обновления ссылок в Next.js 13 можно использовать кодмод new-link.

Компонент <Script>

Поведение next/script было обновлено для поддержки как pages, так и app, но для плавной миграции необходимо внести некоторые изменения:

  • Перенесите все скрипты beforeInteractive, которые ранее были в _document.js, в корневой файл макета (app/layout.tsx).
  • Экспериментальная стратегия worker пока не работает в app, и скрипты с этой стратегией необходимо либо удалить, либо изменить на другую стратегию (например, lazyOnload).
  • Обработчики onLoad, onReady и onError не работают в серверных компонентах, поэтому их нужно перенести в клиентский компонент или удалить.

Оптимизация шрифтов

Ранее Next.js помогал оптимизировать шрифты с помощью инлайнинга CSS шрифтов. Версия 13 представляет новый модуль next/font, который позволяет настраивать загрузку шрифтов, сохраняя высокую производительность и конфиденциальность. next/font поддерживается как в pages, так и в app.

Хотя инлайнинг CSS по-прежнему работает в pages, он не работает в app. Вместо этого следует использовать next/font.

Подробнее см. на странице Оптимизация шрифтов.

Миграция с pages на app

🎥 Видео: Узнайте, как постепенно внедрить App Router → YouTube (16 минут).

Переход на App Router может быть первым опытом работы с функциями React, на которых построен Next.js, такими как серверные компоненты, Suspense и другие. В сочетании с новыми функциями Next.js, такими как специальные файлы и макеты, миграция означает изучение новых концепций, моделей мышления и изменений в поведении.

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

  • Директория app поддерживает вложенные маршруты и макеты. Подробнее.
  • Используйте вложенные папки для определения маршрутов и специальный файл page.js для публичного доступа к сегменту маршрута. Подробнее.
  • Специальные файлы используются для создания UI для каждого сегмента маршрута. Наиболее распространённые специальные файлы — page.js и layout.js.
    • Используйте page.js для определения уникального UI маршрута.
    • Используйте layout.js для определения UI, общего для нескольких маршрутов.
    • Для специальных файлов можно использовать расширения .js, .jsx или .tsx.
  • В директории app можно размещать другие файлы, такие как компоненты, стили, тесты и т. д. Подробнее.
  • Функции получения данных, такие как getServerSideProps и getStaticProps, заменены на новый API внутри app. getStaticPaths заменён на generateStaticParams.
  • pages/_app.js и pages/_document.js заменены на единый корневой макет app/layout.js. Подробнее.
  • pages/_error.js заменён на более детализированные специальные файлы error.js. Подробнее.
  • pages/404.js заменён на файл not-found.js.
  • API Routes pages/api/* заменены на специальный файл route.js (Route Handler).

Шаг 1: Создание директории app

Обновитесь до последней версии Next.js (требуется 13.4 или выше):

npm install next@latest

Затем создайте новую директорию app в корне проекта (или в директории src/).

Шаг 2: Создание корневого макета

Создайте новый файл app/layout.tsx внутри директории app. Это корневой макет, который будет применяться ко всем маршрутам внутри app.

export default function RootLayout({
  // Макеты должны принимать проп children.
  // Это будет заполнено вложенными макетами или страницами
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
export default function RootLayout({
  // Макеты должны принимать проп children.
  // Это будет заполнено вложенными макетами или страницами
  children,
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
  • Директория app обязательно должна включать корневой макет.
  • Корневой макет должен определять теги <html> и <body>, так как Next.js не создаёт их автоматически.
  • Корневой макет заменяет файлы pages/_app.tsx и pages/_document.tsx.
  • Для файлов макетов можно использовать расширения .js, .jsx или .tsx.

Для управления элементами <head> можно использовать встроенную поддержку SEO:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Home',
  description: 'Welcome to Next.js',
}
export const metadata = {
  title: 'Home',
  description: 'Welcome to Next.js',
}

Миграция _document.js и _app.js

Если у вас есть существующие файлы _app или _document, вы можете скопировать их содержимое (например, глобальные стили) в корневой макет (app/layout.tsx). Стили в app/layout.tsx не будут применяться к pages/*. Следует сохранить _app/_document во время миграции, чтобы маршруты pages/* продолжали работать. После полной миграции их можно безопасно удалить.

Если вы используете провайдеры React Context, их нужно перенести в клиентский компонент.

Миграция шаблона getLayout() на макеты (опционально)

Next.js рекомендовал добавлять свойство к компонентам страниц для реализации макетов на уровне страниц в директории pages. Этот шаблон можно заменить на встроенную поддержку вложенных макетов в директории app.

Пример до и после

До

components/DashboardLayout.js
export default function DashboardLayout({ children }) {
  return (
    <div>
      <h2>My Dashboard</h2>
      {children}
    </div>
  )
}
pages/dashboard/index.js
import DashboardLayout from '../components/DashboardLayout'

export default function Page() {
  return <p>My Page</p>
}

Page.getLayout = function getLayout(page) {
  return <DashboardLayout>{page}</DashboardLayout>
}

После

  • Удалите свойство Page.getLayout из pages/dashboard/index.js и следуйте шагам миграции страниц в директорию app.

    app/dashboard/page.js
    export default function Page() {
      return <p>My Page</p>
    }
  • Перенесите содержимое DashboardLayout в новый клиентский компонент, чтобы сохранить поведение директории pages.

    app/dashboard/DashboardLayout.js
    'use client' // Эта директива должна быть в начале файла, перед любыми импортами.
    
    // Это клиентский компонент
    export default function DashboardLayout({ children }) {
      return (
        <div>
          <h2>My Dashboard</h2>
          {children}
        </div>
      )
    }
  • Импортируйте DashboardLayout в новый файл layout.js внутри директории app.

    app/dashboard/layout.js
    import DashboardLayout from './DashboardLayout'
    
    // Это серверный компонент
    export default function Layout({ children }) {
      return <DashboardLayout>{children}</DashboardLayout>
    }
  • Можно постепенно переносить неинтерактивные части DashboardLayout.js (клиентский компонент) в layout.js (серверный компонент), чтобы уменьшить объём JavaScript, отправляемого клиенту.

Шаг 3: Миграция next/head

В директории pages React-компонент next/head используется для управления элементами <head>, такими как title и meta. В директории app next/head заменён на встроенную поддержку SEO.

До:

import Head from 'next/head'

export default function Page() {
  return (
    <>
      <Head>
        <title>My page title</title>
      </Head>
    </>
  )
}
import Head from 'next/head'

export default function Page() {
  return (
    <>
      <Head>
        <title>My page title</title>
      </Head>
    </>
  )
}

После:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Page Title',
}

export default function Page() {
  return '...'
}
export const metadata = {
  title: 'My Page Title',
}

export default function Page() {
  return '...'
}

См. все варианты метаданных.

Шаг 4: Миграция страниц

Рекомендуем разбить миграцию страницы на два основных шага:

  • Шаг 1: Перенести экспортируемый по умолчанию компонент страницы в новый клиентский компонент.
  • Шаг 2: Импортировать новый клиентский компонент в файл page.js внутри директории app.

Полезно знать: Это самый простой путь миграции, так как он наиболее схож с поведением директории pages.

Шаг 1: Создание нового клиентского компонента

  • Создайте новый отдельный файл внутри директории app (например, app/home-page.tsx или аналогичный), который экспортирует клиентский компонент. Чтобы определить клиентские компоненты, добавьте директиву 'use client' в начало файла (перед любыми импортами).
    • Аналогично маршрутизатору страниц (Pages Router), есть этап оптимизации для предварительного рендеринга клиентских компонентов в статический HTML при первоначальной загрузке страницы.
  • Перенесите экспортируемый по умолчанию компонент страницы из pages/index.js в app/home-page.tsx.
'use client'

// Это клиентский компонент (аналогично компонентам в директории `pages`)
// Он получает данные через пропсы, имеет доступ к состоянию и эффектам и
// предварительно рендерится на сервере при первоначальной загрузке страницы.
export default function HomePage({ recentPosts }) {
  return (
    <div>
      {recentPosts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}
'use client'

// Это клиентский компонент. Он получает данные через пропсы и
// имеет доступ к состоянию и эффектам, как и компоненты страниц
// в директории `pages`.
export default function HomePage({ recentPosts }) {
  return (
    <div>
      {recentPosts.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  )
}

Шаг 2: Создание новой страницы

  • Создайте новый файл app/page.tsx внутри директории app. По умолчанию это серверный компонент.

  • Импортируйте клиентский компонент home-page.tsx в страницу.

  • Если вы получали данные в pages/index.js, перенесите логику получения данных непосредственно в серверный компонент, используя новый API для получения данных. Подробнее см. в руководстве по обновлению методов получения данных.

    // Импортируйте ваш клиентский компонент
    import HomePage from './home-page'
    
    async function getPosts() {
      const res = await fetch('https://...')
      const posts = await res.json()
      return posts
    }
    
    export default async function Page() {
      // Получайте данные напрямую в серверном компоненте
      const recentPosts = await getPosts()
      // Передавайте полученные данные в ваш клиентский компонент
      return <HomePage recentPosts={recentPosts} />
    }
    // Импортируйте ваш клиентский компонент
    import HomePage from './home-page'
    
    async function getPosts() {
      const res = await fetch('https://...')
      const posts = await res.json()
      return posts
    }
    
    export default async function Page() {
      // Получайте данные напрямую в серверном компоненте
      const recentPosts = await getPosts()
      // Передавайте полученные данные в ваш клиентский компонент
      return <HomePage recentPosts={recentPosts} />
    }
  • Если ваша предыдущая страница использовала useRouter, вам нужно обновить её до новых хуков маршрутизации. Узнать больше.

  • Запустите сервер разработки и перейдите по адресу http://localhost:3000. Вы должны увидеть ваш существующий маршрут index, теперь обслуживаемый через директорию app.

Шаг 5: Миграция хуков маршрутизации

Добавлен новый маршрутизатор для поддержки нового поведения в директории app.

В app следует использовать три новых хука, импортируемых из next/navigation: useRouter(), usePathname() и useSearchParams().

  • Новый хук useRouter импортируется из next/navigation и имеет другое поведение по сравнению с хуком useRouter в pages, который импортируется из next/router.
  • Новый useRouter не возвращает строку pathname. Вместо этого используйте отдельный хук usePathname.
  • Новый useRouter не возвращает объект query. Параметры поиска и параметры динамических маршрутов теперь разделены. Вместо этого используйте хуки useSearchParams и useParams.
  • Вы можете использовать useSearchParams и usePathname вместе для отслеживания изменений страницы. Подробнее см. в разделе События маршрутизатора (Router Events).
  • Эти новые хуки поддерживаются только в клиентских компонентах. Они не могут использоваться в серверных компонентах.
'use client'

import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export default function ExampleClientComponent() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  // ...
}
'use client'

import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export default function ExampleClientComponent() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  // ...
}

Кроме того, новый хук useRouter имеет следующие изменения:

  • isFallback удалён, так как fallback был заменён.
  • Значения locale, locales, defaultLocales, domainLocales удалены, так как встроенные функции i18n Next.js больше не нужны в директории app. Подробнее о i18n.
  • basePath удалён. Альтернатива не будет частью useRouter. Это ещё не реализовано.
  • asPath удалён, так как концепция as была удалена из нового маршрутизатора.
  • isReady удалён, так как больше не нужен. Во время статического рендеринга (static rendering) любой компонент, использующий хук useSearchParams(), пропустит этап предварительного рендеринга и вместо этого будет отрендерен на клиенте во время выполнения.
  • route удалён. Альтернативой являются usePathname или useSelectedLayoutSegments().

См. справочник API useRouter().

Совместное использование компонентов между pages и app

Чтобы сохранить совместимость компонентов между маршрутизаторами pages и app, обратитесь к хуку useRouter из next/compat/router. Это хук useRouter из директории pages, но предназначенный для использования при совместном использовании компонентов между маршрутизаторами. Когда вы будете готовы использовать его только в маршрутизаторе app, обновитесь до нового useRouter из next/navigation.

Шаг 6: Миграция методов получения данных

В директории pages используются getServerSideProps и getStaticProps для получения данных для страниц. В директории app эти предыдущие функции получения данных заменены на более простой API, построенный на основе fetch() и асинхронных серверных компонентов React.

export default async function Page() {
  // Этот запрос должен кэшироваться до ручной инвалидации.
  // Аналогично `getStaticProps`.
  // `force-cache` установлен по умолчанию и может быть опущен.
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })

  // Этот запрос должен перезапрашиваться при каждом запросе.
  // Аналогично `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })

  // Этот запрос должен кэшироваться с временем жизни 10 секунд.
  // Аналогично `getStaticProps` с опцией `revalidate`.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })

  return <div>...</div>
}
export default async function Page() {
  // Этот запрос должен кэшироваться до ручной инвалидации.
  // Аналогично `getStaticProps`.
  // `force-cache` установлен по умолчанию и может быть опущен.
  const staticData = await fetch(`https://...`, { cache: 'force-cache' })

  // Этот запрос должен перезапрашиваться при каждом запросе.
  // Аналогично `getServerSideProps`.
  const dynamicData = await fetch(`https://...`, { cache: 'no-store' })

  // Этот запрос должен кэшироваться с временем жизни 10 секунд.
  // Аналогично `getStaticProps` с опцией `revalidate`.
  const revalidatedData = await fetch(`https://...`, {
    next: { revalidate: 10 },
  })

  return <div>...</div>
}

Рендеринг на стороне сервера (getServerSideProps)

В директории pages getServerSideProps используется для получения данных на сервере и передачи пропсов в экспортируемый по умолчанию React-компонент в файле. Исходный HTML для страницы предварительно рендерится на сервере, после чего страница "гидратируется" в браузере (становится интерактивной).

pages/dashboard.js
// Директория `pages`

export async function getServerSideProps() {
  const res = await fetch(`https://...`)
  const projects = await res.json()

  return { props: { projects } }
}

export default function Dashboard({ projects }) {
  return (
    <ul>
      {projects.map((project) => (
        <li key={project.id}>{project.name}</li>
      ))}
    </ul>
  )
}

В маршрутизаторе приложения (App Router) мы можем размещать получение данных внутри наших React-компонентов, используя серверные компоненты (Server Components). Это позволяет отправлять меньше JavaScript на клиент, сохраняя при этом отрендеренный HTML с сервера.

Установив опцию cache в no-store, мы можем указать, что полученные данные никогда не должны кэшироваться. Это аналогично getServerSideProps в директории pages.

// Директория `app`

// Эта функция может называться как угодно
async function getProjects() {
  const res = await fetch(`https://...`, { cache: 'no-store' })
  const projects = await res.json()

  return projects
}

export default async function Dashboard() {
  const projects = await getProjects()

  return (
    <ul>
      {projects.map((project) => (
        <li key={project.id}>{project.name}</li>
      ))}
    </ul>
  )
}
// Директория `app`

// Эта функция может называться как угодно
async function getProjects() {
  const res = await fetch(`https://...`, { cache: 'no-store' })
  const projects = await res.json()

  return projects
}

export default async function Dashboard() {
  const projects = await getProjects()

  return (
    <ul>
      {projects.map((project) => (
        <li key={project.id}>{project.name}</li>
      ))}
    </ul>
  )
}

Доступ к объекту запроса

В директории pages вы можете получать данные запроса на основе Node.js HTTP API.

Например, вы можете получить объект req из getServerSideProps и использовать его для получения cookies и заголовков запроса.

pages/index.js
// Директория `pages`

export async function getServerSideProps({ req, query }) {
  const authHeader = req.getHeaders()['authorization'];
  const theme = req.cookies['theme'];

  return { props: { ... }}
}

export default function Page(props) {
  return ...
}

Директория app предоставляет новые функции только для чтения для получения данных запроса:

// Директория `app`
import { cookies, headers } from 'next/headers'

async function getData() {
  const authHeader = (await headers()).get('authorization')

  return '...'
}

export default async function Page() {
  // Вы можете использовать `cookies` или `headers` внутри серверных компонентов
  // напрямую или в вашей функции получения данных
  const theme = (await cookies()).get('theme')
  const data = await getData()
  return '...'
}
// Директория `app`
import { cookies, headers } from 'next/headers'

async function getData() {
  const authHeader = (await headers()).get('authorization')

  return '...'
}

export default async function Page() {
  // Вы можете использовать `cookies` или `headers` внутри серверных компонентов
  // напрямую или в вашей функции получения данных
  const theme = (await cookies()).get('theme')
  const data = await getData()
  return '...'
}

Генерация статических сайтов (getStaticProps)

В директории pages функция getStaticProps используется для предварительного рендеринга страницы во время сборки. Эта функция может использоваться для получения данных из внешнего API или непосредственно из базы данных и передачи этих данных всей странице во время её генерации.

pages/index.js
// Директория `pages`

export async function getStaticProps() {
  const res = await fetch(`https://...`)
  const projects = await res.json()

  return { props: { projects } }
}

export default function Index({ projects }) {
  return projects.map((project) => <div>{project.name}</div>)
}

В директории app получение данных с помощью fetch() по умолчанию будет использовать cache: 'force-cache', что закэширует данные запроса до ручной инвалидации. Это аналогично getStaticProps в директории pages.

app/page.js
// Директория `app`

// Эта функция может называться как угодно
async function getProjects() {
  const res = await fetch(`https://...`)
  const projects = await res.json()

  return projects
}

export default async function Index() {
  const projects = await getProjects()

  return projects.map((project) => <div>{project.name}</div>)
}

Динамические пути (getStaticPaths)

В директории pages функция getStaticPaths используется для определения динамических путей, которые должны быть предварительно отрендерены во время сборки.

pages/posts/[id].js
// Директория `pages`
import PostLayout from '@/components/post-layout'

export async function getStaticPaths() {
  return {
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
  }
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return { props: { post } }
}

export default function Post({ post }) {
  return <PostLayout post={post} />
}

В директории app функция getStaticPaths заменена на generateStaticParams.

Функция generateStaticParams работает аналогично getStaticPaths, но имеет упрощённый API для возврата параметров маршрута и может использоваться внутри макетов (layouts). В отличие от getStaticPaths, generateStaticParams возвращает массив сегментов вместо массива вложенных объектов param или строки с разрешёнными путями.

app/posts/[id]/page.js
// Директория `app`
import PostLayout from '@/components/post-layout'

export async function generateStaticParams() {
  return [{ id: '1' }, { id: '2' }]
}

async function getPost(params) {
  const res = await fetch(`https://.../posts/${(await params).id}`)
  const post = await res.json()

  return post
}

export default async function Post({ params }) {
  const post = await getPost(params)

  return <PostLayout post={post} />
}

Использование имени generateStaticParams более уместно для новой модели в директории app, чем getStaticPaths. Префикс get заменён на более описательный generate, что лучше соответствует новой модели, где getStaticProps и getServerSideProps больше не нужны. Суффикс Paths заменён на Params, что лучше подходит для вложенной маршрутизации с несколькими динамическими сегментами.


Замена fallback

В директории pages свойство fallback, возвращаемое из getStaticPaths, используется для определения поведения страницы, которая не была предварительно отрендерена во время сборки. Это свойство может быть установлено в true для показа резервной страницы во время генерации, false для показа страницы 404 или blocking для генерации страницы во время запроса.

pages/posts/[id].js
// Директория `pages`

export async function getStaticPaths() {
  return {
    paths: [],
    fallback: 'blocking'
  };
}

export async function getStaticProps({ params }) {
  ...
}

export default function Post({ post }) {
  return ...
}

В директории app свойство config.dynamicParams контролирует обработку параметров, не включённых в generateStaticParams:

  • true: (по умолчанию) Динамические сегменты, не включённые в generateStaticParams, генерируются по запросу.
  • false: Динамические сегменты, не включённые в generateStaticParams, вернут 404.

Это заменяет опцию fallback: true | false | 'blocking' функции getStaticPaths в директории pages. Опция fallback: 'blocking' не включена в dynamicParams, так как разница между 'blocking' и true незначительна при использовании потоковой передачи.

app/posts/[id]/page.js
// Директория `app`

export const dynamicParams = true;

export async function generateStaticParams() {
  return [...]
}

async function getPost(params) {
  ...
}

export default async function Post({ params }) {
  const post = await getPost(params);

  return ...
}

При установке dynamicParams в true (по умолчанию), когда запрашивается сегмент маршрута, который не был сгенерирован, он будет отрендерен на сервере и закэширован.

Инкрементальная статическая регенерация (getStaticProps с revalidate)

В директории pages функция getStaticProps позволяет добавить поле revalidate для автоматической регенерации страницы через определённый промежуток времени.

pages/index.js
// Директория `pages`

export async function getStaticProps() {
  const res = await fetch(`https://.../posts`)
  const posts = await res.json()

  return {
    props: { posts },
    revalidate: 60,
  }
}

export default function Index({ posts }) {
  return (
    <Layout>
      <PostList posts={posts} />
    </Layout>
  )
}

В директории app при получении данных с помощью fetch() можно использовать revalidate, который будет кэшировать запрос на указанное количество секунд.

app/page.js
// Директория `app`

async function getPosts() {
  const res = await fetch(`https://.../posts`, { next: { revalidate: 60 } })
  const data = await res.json()

  return data.posts
}

export default async function PostList() {
  const posts = await getPosts()

  return posts.map((post) => <div>{post.name}</div>)
}

API-маршруты

API-маршруты продолжают работать в директории pages/api без изменений. Однако в директории app они заменены на Обработчики маршрутов (Route Handlers).

Обработчики маршрутов позволяют создавать пользовательские обработчики запросов для заданного маршрута с использованием Web API Request и Response.

export async function GET(request: Request) {}
export async function GET(request) {}

Полезно знать: Если вы ранее использовали API-маршруты для вызова внешнего API из клиента, теперь вы можете использовать Серверные компоненты (Server Components) для безопасного получения данных. Подробнее о получении данных.

Одностраничные приложения (SPA)

Если вы одновременно переходите на Next.js с Одностраничного приложения (SPA), ознакомьтесь с нашей документацией для получения дополнительной информации.

Шаг 7: Стилизация

В директории pages глобальные стили ограничены только файлом pages/_app.js. В директории app это ограничение снято. Глобальные стили могут быть добавлены в любой макет, страницу или компонент.

Tailwind CSS

Если вы используете Tailwind CSS, вам нужно добавить директорию app в файл tailwind.config.js:

tailwind.config.js
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}', // <-- Добавьте эту строку
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
  ],
}

Также необходимо импортировать глобальные стили в файле app/layout.js:

app/layout.js
import '../styles/globals.css'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Подробнее о стилизации с Tailwind CSS

Использование App Router вместе с Pages Router

При переходе между маршрутами, обслуживаемыми разными роутерами Next.js, будет происходить жёсткая навигация. Автоматический префетчинг ссылок с next/link не будет работать между роутерами.

Вместо этого вы можете оптимизировать навигацию между App Router и Pages Router для сохранения префетчинга и быстрых переходов между страницами. Подробнее.

Codemods

Next.js предоставляет преобразования Codemod для помощи в обновлении кодовой базы при устаревании функциональности. Подробнее см. Codemods.