Миграция с Create React App

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

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

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

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

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

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

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

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

Каскадные сетевые запросы

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

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

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

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

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

Выбор стратегии загрузки данных

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

Middleware

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

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

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

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

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

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

Сначала установите next как зависимость:

Terminal
npm install next@latest

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

Создайте файл next.config.mjs в корне вашего проекта. Этот файл будет содержать параметры конфигурации Next.js.

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // Генерирует одностраничное приложение (SPA).
  distDir: './dist', // Изменяет директорию сборки на `./dist/`.
}

export default nextConfig

Шаг 3: Обновление конфигурации TypeScript

Если вы используете TypeScript, обновите файл tsconfig.json следующими изменениями для совместимости с Next.js:

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "strictNullChecks": true
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "./dist/types/**/*.ts"
  ],
  "exclude": ["node_modules"]
}

Подробнее о настройке TypeScript можно узнать в документации Next.js.

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

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

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

На этом шаге вы преобразуете файл index.html в корневой макет:

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

Полезно знать: Для файлов макета можно использовать расширения .js, .jsx или .tsx.

Скопируйте содержимое вашего файла index.html в созданный компонент <RootLayout>, заменив теги body.div#root и body.script на <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 также поддерживает эти опции.

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

Next.js уже включает по умолчанию теги meta charset и meta viewport, поэтому их можно безопасно удалить из <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 и возможность делиться вашими страницами.

Шаг 6: Стили

Как и Create React App, Next.js имеет встроенную поддержку CSS Modules.

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

import '../index.css'

// ...

Если вы используете Tailwind, вам нужно установить postcss и autoprefixer:

Terminal
npm install postcss autoprefixer

Затем создайте файл postcss.config.js в корне вашего проекта:

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

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

В Next.js точка входа для вашего приложения объявляется созданием файла page.tsx. Ближайший аналог этого файла в CRA — ваш файл src/index.tsx. На этом шаге вы настроите точку входа вашего приложения.

Создайте директорию [[...slug]] в вашей директории app.

Поскольку это руководство сначала настраивает Next.js как SPA (одностраничное приложение), вам нужно, чтобы точка входа перехватывала все возможные маршруты вашего приложения. Для этого создайте новую директорию [[...slug]] в директории app.

Эта директория называется опциональным перехватывающим сегментом маршрута. Next.js использует файловую систему для маршрутизации, где директории определяют маршруты. Эта специальная директория гарантирует, что все маршруты вашего приложения будут направлены в содержащийся в ней файл page.tsx.

Создайте новый файл page.tsx внутри директории app/[[...slug]] со следующим содержимым:

import '../../index.css'

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

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

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

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

Этот файл — Server Component. При запуске next build файл предварительно рендерится в статический ресурс. Он не требует динамического кода.

Этот файл импортирует наш глобальный CSS и сообщает generateStaticParams, что мы собираемся сгенерировать только один маршрут — корневой маршрут /.

Теперь перенесём остальную часть нашего CRA-приложения, которая будет работать только на клиенте.

'use client'

import React from 'react'
import dynamic from 'next/dynamic'

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

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

import React from 'react'
import dynamic from 'next/dynamic'

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

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

Этот файл — Client Component, определённый директивой 'use client'. Клиентские компоненты всё ещё предварительно рендерятся в HTML на сервере перед отправкой клиенту.

Поскольку мы хотим начать с чисто клиентского приложения, мы можем настроить Next.js на отключение предварительного рендеринга для компонента App и ниже.

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

Теперь обновите точку входа, чтобы использовать новый компонент:

import '../../index.css'
import { ClientOnly } from './client'

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

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

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

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

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

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

App.tsx
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. Пока их можно игнорировать — они будут исправлены к концу этого руководства.

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

Next.js поддерживает переменные окружения в файлах .env, аналогично CRA.

Основное отличие — префикс для переменных, доступных на стороне клиента. Замените все переменные с префиксом REACT_APP_ на NEXT_PUBLIC_.

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

Теперь вы можете запустить приложение, чтобы проверить успешность миграции на Next.js. Но сначала обновите scripts в package.json командами Next.js и добавьте .next, next-env.d.ts и dist в файл .gitignore:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}
.gitignore
# ...
.next
next-env.d.ts
dist

Теперь выполните npm run dev и откройте http://localhost:3000. Ваше приложение должно работать на Next.js.

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

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

  • Удалите src/index.tsx
  • Удалите public/index.html
  • Удалите настройки reportWebVitals
  • Удалите зависимости CRA (react-scripts)

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

Create React App и Next.js по умолчанию используют webpack для сборки.

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

Кроме того, Next.js поддерживает Turbopack через команду next dev --turbo для улучшения производительности локальной разработки. Turbopack также поддерживает некоторые загрузчики webpack для совместимости и постепенного перехода.

Дальнейшие шаги

Если все прошло успешно, у вас теперь работает приложение на Next.js в виде одностраничного приложения. Однако вы пока не используете большинство преимуществ Next.js, но можете начать вносить постепенные изменения. Вот что можно сделать дальше:

Полезно знать: При использовании статического экспорта в настоящее время не поддерживается использование хука useParams.