Как мигрировать с Create React App на Next.js

Это руководство поможет вам перенести существующий сайт с Create React App (CRA) на Next.js.

Почему стоит перейти?

Есть несколько причин, по которым вы можете захотеть перейти с Create React App на Next.js:

Медленная загрузка начальной страницы

Create React App использует исключительно клиентский React. Приложения, работающие только на клиентской стороне (также известные как одностраничные приложения (SPA), часто страдают от медленной загрузки начальной страницы. Это происходит по нескольким причинам:

  1. Браузеру нужно дождаться загрузки и выполнения кода React и всего бандла приложения, прежде чем ваш код сможет отправить запросы на загрузку данных.
  2. Код вашего приложения растёт с каждым новым функционалом и зависимостью.

Отсутствие автоматического разделения кода

Проблему медленной загрузки можно частично решить с помощью разделения кода. Однако при ручном разделении кода вы можете непреднамеренно создать "водопады" (waterfalls) сетевых запросов. Next.js предоставляет автоматическое разделение кода и tree-shaking, встроенные в его роутер и процесс сборки.

Водопады сетевых запросов

Распространённая причина плохой производительности — последовательные клиент-серверные запросы для получения данных. Один из подходов к получению данных в SPA — отобразить заглушку, а затем загрузить данные после монтирования компонента. К сожалению, дочерний компонент может начать загрузку данных только после того, как родительский компонент завершит загрузку своих данных, что приводит к "водопаду" запросов.

Хотя Next.js поддерживает клиентское получение данных, он также позволяет перенести его на сервер. Это часто полностью устраняет клиент-серверные водопады.

Быстрые и контролируемые состояния загрузки

Со встроенной поддержкой стриминга через React Suspense вы можете определить, какие части вашего интерфейса загружаются первыми и в каком порядке, не создавая при этом водопадов сетевых запросов.

Это позволяет создавать страницы, которые загружаются быстрее и исключают сдвиги макета.

Выбор стратегии получения данных

В зависимости от ваших потребностей, Next.js позволяет выбирать стратегию получения данных на уровне страницы или компонента. Например, вы можете получать данные из вашей CMS и рендерить блог-посты во время сборки (SSG) для быстрой загрузки или загружать данные во время запроса (SSR), когда это необходимо.

Middleware

Next.js Middleware позволяет выполнять код на сервере до завершения запроса. Например, вы можете избежать мелькания неаутентифицированного контента, перенаправляя пользователя на страницу входа в middleware для страниц, доступных только аутентифицированным пользователям. Также middleware можно использовать для A/B-тестирования, экспериментов и интернационализации.

Встроенные оптимизации

Изображения, шрифты и сторонние скрипты часто сильно влияют на производительность приложения. Next.js включает специализированные компоненты и API, которые автоматически оптимизируют их для вас.

Шаги миграции

Наша цель — как можно быстрее получить работающее Next.js-приложение, чтобы затем постепенно внедрять функции Next.js. Для начала мы будем рассматривать ваше приложение как чисто клиентское (SPA), не заменяя сразу существующий роутер. Это уменьшает сложность и количество конфликтов при слиянии.

Примечание: Если вы используете расширенные конфигурации CRA, такие как пользовательское поле homepage в package.json, пользовательский service worker или специфические настройки Babel/webpack, обратитесь к разделу Дополнительные соображения в конце этого руководства за советами по их адаптации в Next.js.

Шаг 1: Установка зависимости Next.js

Установите Next.js в ваш существующий проект:

Terminal
npm install next@latest

Шаг 2: Создание конфигурационного файла Next.js

Создайте файл next.config.ts в корне вашего проекта (на том же уровне, что и package.json). Этот файл содержит настройки конфигурации Next.js.

next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export', // Экспортирует одностраничное приложение (SPA)
  distDir: 'build', // Изменяет директорию сборки на `build`
}

export default nextConfig

Примечание: Использование output: 'export' означает, что вы делаете статический экспорт. У вас не будет доступа к серверным функциям, таким как SSR или API. Вы можете удалить эту строку, чтобы использовать серверные функции Next.js.

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

Приложение Next.js с App Router должно включать корневой макет, который является React Server Component, оборачивающим все ваши страницы.

Ближайший аналог корневого макета в приложении CRA — public/index.html, который включает теги <html>, <head> и <body>.

  1. Создайте новую директорию app внутри вашей папки src (или в корне проекта, если вы предпочитаете app в корне).
  2. Внутри директории app создайте файл layout.tsx (или layout.js):
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return '...'
}
export default function RootLayout({ children }) {
  return '...'
}

Теперь скопируйте содержимое вашего старого index.html в этот компонент <RootLayout>. Замените body div#rootbody noscript) на <div id="root">{children}</div>.

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Полезно знать: Next.js игнорирует public/manifest.json CRA, дополнительную иконографию и конфигурацию тестирования по умолчанию. Если они вам нужны, Next.js поддерживает их с помощью Metadata API и настройки тестирования.

Шаг 4: Метаданные

Next.js автоматически включает теги <meta charset="UTF-8" /> и <meta name="viewport" content="width=device-width, initial-scale=1" />, поэтому вы можете удалить их из <head>:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Любые файлы метаданных, такие как favicon.ico, icon.png, robots.txt, автоматически добавляются в тег <head> приложения, если они размещены в корне директории app. После перемещения всех поддерживаемых файлов в директорию app вы можете безопасно удалить их теги <link>:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>React App</title>
        <meta name="description" content="Web site created..." />
      </head>
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Наконец, Next.js может управлять вашими последними тегами <head> с помощью Metadata API. Перенесите последние метаданные в экспортируемый объект metadata:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}
export const metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

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

С этими изменениями вы перешли от объявления всего в index.html к использованию конвенционного подхода Next.js, встроенного в фреймворк (Metadata API). Этот подход позволяет вам легче улучшать SEO и доступность ваших страниц для совместного использования.

Шаг 5: Стили

Как и CRA, Next.js поддерживает CSS Modules из коробки. Также поддерживается импорт глобальных CSS.

Если у вас есть глобальный CSS-файл, импортируйте его в app/layout.tsx:

import '../index.css'

export const metadata = {
  title: 'React App',
  description: 'Web site created with Next.js.',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Если вы используете Tailwind CSS, ознакомьтесь с нашей документацией по установке.

Шаг 6: Создание точки входа

Create React App использует src/index.tsx (или index.js) в качестве точки входа. В Next.js (App Router) каждая папка внутри директории app соответствует маршруту, и каждая папка должна содержать page.tsx.

Поскольку мы хотим сохранить приложение как SPA и перехватывать все маршруты, мы будем использовать опциональный catch-all маршрут.

  1. Создайте директорию [[...slug]] внутри app.
app
 [[...slug]]
 page.tsx
 layout.tsx
  1. Добавьте следующее в page.tsx:
export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // Мы обновим это
}
export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return '...' // Мы обновим это
}

Это указывает Next.js сгенерировать один маршрут для пустого slug (/), эффективно сопоставляя все маршруты с одной страницей. Эта страница является Server Component, предварительно отрендеренной в статический HTML.

Шаг 7: Добавление клиентской точки входа

Далее мы встроим корневой компонент App из CRA в Client Component, чтобы вся логика оставалась на клиентской стороне. Если вы впервые используете Next.js, стоит знать, что клиентские компоненты (по умолчанию) всё равно предварительно рендерятся на сервере. Можно считать, что они обладают дополнительной возможностью выполнения клиентского JavaScript.

Создайте client.tsx (или client.js) в app/[[...slug]]/:

'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../App'), { ssr: false })

export function ClientOnly() {
  return <App />
}
  • Директива 'use client' делает этот файл Client Component.
  • Динамический импорт с ssr: false отключает серверный рендеринг для компонента <App />, делая его чисто клиентским (SPA).

Теперь обновите ваш page.tsx (или page.js), чтобы использовать новый компонент:

import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

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

Шаг 8: Обновление статических импортов изображений

В CRA импорт файла изображения возвращает его публичный URL в виде строки:

import image from './img.png'

export default function App() {
  return <img src={image} />
}

В Next.js статический импорт изображений возвращает объект. Этот объект можно использовать напрямую с компонентом <Image> в Next.js, либо можно использовать свойство src этого объекта с существующим тегом <img>.

Компонент <Image> предоставляет дополнительные преимущества, такие как автоматическая оптимизация изображений. Компонент <Image> автоматически устанавливает атрибуты width и height результирующего тега <img> на основе размеров изображения. Это предотвращает сдвиги макета при загрузке изображения. Однако это может вызвать проблемы, если в вашем приложении есть изображения, у которых стилизована только одна из размерностей, а другая не установлена в auto. Если размерность не установлена в auto, она будет по умолчанию принимать значение атрибута <img>, что может привести к искажению изображения.

Использование тега <img> уменьшит количество изменений в вашем приложении и предотвратит указанные выше проблемы. В дальнейшем вы можете перейти на компонент <Image>, чтобы воспользоваться оптимизацией изображений, настроив загрузчик, или перейдя на стандартный сервер Next.js, который поддерживает автоматическую оптимизацию изображений.

Преобразуйте абсолютные пути импорта для изображений из /public в относительные:

// Было
import logo from '/logo.png'

// Стало
import logo from '../public/logo.png'

Передавайте свойство src изображения вместо всего объекта в тег <img>:

// Было
<img src={logo} />

// Стало
<img src={logo.src} />

Альтернативно, вы можете ссылаться на публичный URL изображения на основе имени файла. Например, public/logo.png будет доступен по пути /logo.png в вашем приложении, что можно использовать в качестве значения src.

Предупреждение: Если вы используете TypeScript, при обращении к свойству src могут возникнуть ошибки типов. Чтобы их исправить, добавьте next-env.d.ts в массив include вашего файла tsconfig.json. Next.js автоматически создаст этот файл при запуске приложения на шаге 9.

Шаг 9: Миграция переменных окружения

Next.js поддерживает переменные окружения аналогично CRA, но требует префикс NEXT_PUBLIC_ для любых переменных, которые должны быть доступны в браузере.

Основное отличие — префикс для экспорта переменных окружения на клиентской стороне. Измените все переменные с префиксом REACT_APP_ на NEXT_PUBLIC_.

Шаг 10: Обновление скриптов в package.json

Обновите скрипты в package.json для использования команд Next.js. Также добавьте .next и next-env.d.ts в ваш .gitignore:

package.json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "npx serve@latest ./build"
  }
}
.gitignore
# ...
.next
next-env.d.ts

Теперь вы можете выполнить:

npm run dev

Откройте http://localhost:3000. Вы должны увидеть ваше приложение, работающее на Next.js (в режиме SPA).

Шаг 11: Очистка

Теперь вы можете удалить артефакты, специфичные для Create React App:

  • public/index.html
  • src/index.tsx
  • src/react-app-env.d.ts
  • Настройку reportWebVitals
  • Зависимость react-scripts (удалите её из package.json)

Дополнительные соображения

Использование кастомного homepage в CRA

Если вы использовали поле homepage в package.json CRA для размещения приложения по определённому подпути, вы можете повторить это в Next.js с помощью конфигурации basePath в next.config.ts:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  basePath: '/my-subpath',
  // ...
}

export default nextConfig

Работа с кастомным Service Worker

Если вы использовали сервис-воркер CRA (например, serviceWorker.js из create-react-app), вы можете изучить, как создавать Прогрессивные Веб-Приложения (PWA) с Next.js.

Проксирование API-запросов

Если ваше приложение CRA использовало поле proxy в package.json для перенаправления запросов на бэкенд-сервер, вы можете повторить это с помощью rewrites в Next.js в next.config.ts:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://your-backend.com/:path*',
      },
    ]
  },
}

Кастомная конфигурация Webpack / Babel

Если у вас была кастомная конфигурация webpack или Babel в CRA, вы можете расширить конфигурацию Next.js в next.config.ts:

next.config.ts
import { NextConfig } from 'next'

const nextConfig: NextConfig = {
  webpack: (config, { isServer }) => {
    // Модифицируйте конфигурацию webpack здесь
    return config
  },
}

export default nextConfig

Примечание: Это потребует отключения Turbopack путём удаления --turbopack из скрипта dev.

Настройка TypeScript

Next.js автоматически настраивает TypeScript, если у вас есть tsconfig.json. Убедитесь, что next-env.d.ts указан в массиве include вашего tsconfig.json:

{
  "include": ["next-env.d.ts", "app/**/*", "src/**/*"]
}

Совместимость с бандлерами

И Create React App, и Next.js по умолчанию используют webpack для сборки. Next.js также предлагает Turbopack для более быстрой локальной разработки:

next dev --turbopack

Вы также можете предоставить кастомную конфигурацию webpack, если вам нужно перенести сложные настройки webpack из CRA.

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

Если всё работает, у вас теперь есть функционирующее приложение Next.js, работающее как одностраничное приложение. Вы пока не используете такие возможности Next.js, как рендеринг на стороне сервера (SSR) или файловая маршрутизация, но теперь вы можете внедрять их постепенно:

Примечание: Использование статического экспорта (output: 'export') пока не поддерживает хук useParams и другие серверные функции. Чтобы использовать все возможности Next.js, удалите output: 'export' из вашего next.config.ts.