Как создавать одностраничные приложения (SPA) с помощью Next.js
Next.js полностью поддерживает создание одностраничных приложений (Single-Page Applications, SPA).
Это включает быстрые переходы между маршрутами с предварительной загрузкой, клиентский сбор данных, использование браузерных API, интеграцию со сторонними клиентскими библиотеками, создание статических маршрутов и многое другое.
Если у вас уже есть SPA, вы можете мигрировать на Next.js без значительных изменений кода. Затем Next.js позволяет постепенно добавлять серверные функции по мере необходимости.
Что такое одностраничное приложение (SPA)?
Определение SPA может варьироваться. Мы определим "строгое SPA" как:
- Клиентский рендеринг (CSR): Приложение обслуживается одним HTML-файлом (например,
index.html
). Каждый маршрут, переход между страницами и сбор данных обрабатывается JavaScript в браузере. - Без полной перезагрузки страницы: Вместо запроса нового документа для каждого маршрута клиентский JavaScript изменяет DOM текущей страницы и при необходимости загружает данные.
Строгие SPA часто требуют загрузки большого количества JavaScript перед тем, как страница станет интерактивной. Кроме того, клиентские водопады данных могут быть сложными в управлении. Создание SPA с Next.js помогает решить эти проблемы.
Почему стоит использовать Next.js для SPA?
Next.js может автоматически разделять ваши JavaScript-бандлы и генерировать несколько HTML-точек входа для разных маршрутов. Это позволяет избежать загрузки ненужного JavaScript-кода на стороне клиента, уменьшая размер бандла и ускоряя загрузку страниц.
Компонент next/link
автоматически предзагружает маршруты, обеспечивая быстрые переходы между страницами, как в строгом SPA, но с преимуществом сохранения состояния маршрутизации приложения в URL для ссылок и общего доступа.
Next.js может начинаться как статический сайт или даже строгое SPA, где всё рендерится на стороне клиента. Если ваш проект растёт, Next.js позволяет постепенно добавлять больше серверных функций (например, React Server Components, Server Actions и другие) по мере необходимости.
Примеры
Давайте рассмотрим распространённые шаблоны, используемые для создания SPA, и как Next.js решает их.
Использование хука use
React внутри провайдера контекста
Мы рекомендуем собирать данные в родительском компоненте (или макете), возвращать Promise, а затем разворачивать значение в клиентском компоненте с помощью хука use
React.
Next.js может начать сбор данных раньше на сервере. В этом примере это корневой макет — точка входа в ваше приложение. Сервер может немедленно начать потоковую передачу ответа клиенту.
"Поднимая" сбор данных в корневой макет, Next.js начинает указанные запросы на сервере раньше, чем любые другие компоненты в вашем приложении. Это устраняет клиентские водопады и предотвращает множественные циклы обмена между клиентом и сервером. Это также может значительно улучшить производительность, так как ваш сервер ближе (и в идеале расположен рядом) к вашей базе данных.
Например, обновите ваш корневой макет, чтобы вызвать Promise, но не ожидать его.
import { UserProvider } from './user-provider'
import { getUser } from './user' // какая-то серверная функция
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // НЕ ожидаем
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
import { UserProvider } from './user-provider'
import { getUser } from './user' // какая-то серверная функция
export default function RootLayout({ children }) {
let userPromise = getUser() // НЕ ожидаем
return (
<html lang="en">
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
Хотя вы можете отложить и передать один Promise как проп в клиентский компонент, мы обычно видим этот шаблон в паре с провайдером контекста React. Это обеспечивает более лёгкий доступ из клиентских компонентов с помощью пользовательского хука React.
Вы можете передать Promise в провайдер контекста React:
'use client';
import { createContext, useContext, ReactNode } from 'react';
type User = any;
type UserContextType = {
userPromise: Promise<User | null>;
};
const UserContext = createContext<UserContextType | null>(null);
export function useUser(): UserContextType {
let context = useContext(UserContext);
if (context === null) {
throw new Error('useUser должен использоваться внутри UserProvider');
}
return context;
}
export function UserProvider({
children,
userPromise
}: {
children: ReactNode;
userPromise: Promise<User | null>;
}) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
);
}
'use client'
import { createContext, useContext, ReactNode } from 'react'
const UserContext = createContext(null)
export function useUser() {
let context = useContext(UserContext)
if (context === null) {
throw new Error('useUser должен использоваться внутри UserProvider')
}
return context
}
export function UserProvider({ children, userPromise }) {
return (
<UserContext.Provider value={{ userPromise }}>
{children}
</UserContext.Provider>
)
}
Наконец, вы можете вызвать пользовательский хук useUser()
в любом клиентском компоненте и развернуть Promise:
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}
Компонент, который использует Promise (например, Profile
выше), будет приостановлен. Это позволяет частичную гидратацию. Вы можете увидеть потоковый и предварительно отрендеренный HTML до завершения загрузки JavaScript.
SPA с SWR
SWR — популярная библиотека React для сбора данных.
С SWR 2.3.0 (и React 19+) вы можете постепенно внедрять серверные функции вместе с существующим клиентским кодом сбора данных на основе SWR. Это абстракция приведённого выше шаблона use()
. Это означает, что вы можете перемещать сбор данных между клиентом и сервером или использовать оба варианта:
- Только клиент:
useSWR(key, fetcher)
- Только сервер:
useSWR(key)
+ данные, предоставленные RSC - Смешанный:
useSWR(key, fetcher)
+ данные, предоставленные RSC
Например, оберните ваше приложение в <SWRConfig>
с fallback
:
import { SWRConfig } from 'swr'
import { getUser } from './user' // какая-то серверная функция
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// Мы НЕ ожидаем getUser() здесь
// Только компоненты, читающие эти данные, будут приостановлены
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
import { SWRConfig } from 'swr'
import { getUser } from './user' // какая-то серверная функция
export default function RootLayout({ children }) {
return (
<SWRConfig
value={{
fallback: {
// Мы НЕ ожидаем getUser() здесь
// Только компоненты, читающие эти данные, будут приостановлены
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}
Поскольку это серверный компонент, getUser()
может безопасно читать куки, заголовки или общаться с вашей базой данных. Отдельный API-маршрут не нужен. Клиентские компоненты ниже <SWRConfig>
могут вызвать useSWR()
с тем же ключом, чтобы получить данные пользователя. Код компонента с useSWR
не требует никаких изменений по сравнению с вашим существующим клиентским решением для сбора данных.
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// Тот же шаблон SWR, который вы уже знаете
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
'use client'
import useSWR from 'swr'
export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// Тот же шаблон SWR, который вы уже знаете
const { data, error } = useSWR('/api/user', fetcher)
return '...'
}
Данные fallback
могут быть предварительно отрендерены и включены в начальный HTML-ответ, затем немедленно прочитаны в дочерних компонентах с помощью useSWR
. Опрос, повторная проверка и кэширование SWR по-прежнему работают только на стороне клиента, сохраняя всю интерактивность, необходимую для SPA.
Поскольку начальные данные fallback
автоматически обрабатываются Next.js, теперь вы можете удалить любую условную логику, ранее необходимую для проверки, было ли data
undefined
. Когда данные загружаются, ближайшая граница <Suspense>
будет приостановлена.
SWR | RSC | RSC + SWR | |
---|---|---|---|
SSR данные | |||
Потоковая передача при SSR | |||
Дедупликация запросов | |||
Клиентские функции |
SPA с React Query
Вы можете использовать React Query с Next.js как на клиенте, так и на сервере. Это позволяет создавать как строгие SPA, так и использовать серверные функции Next.js в сочетании с React Query.
Подробнее в документации React Query.
Рендеринг компонентов только в браузере
Клиентские компоненты предварительно рендерятся во время next build
. Если вы хотите отключить предварительный рендеринг для клиентского компонента и загружать его только в среде браузера, вы можете использовать next/dynamic
:
import dynamic from 'next/dynamic'
const ClientOnlyComponent = dynamic(() => import('./component'), {
ssr: false,
})
Это может быть полезно для сторонних библиотек, которые зависят от браузерных API, таких как window
или document
. Вы также можете добавить useEffect
, который проверяет наличие этих API, и если они отсутствуют, вернуть null
или состояние загрузки, которое будет предварительно отрендерено.
Поверхностная маршрутизация на клиенте
Если вы мигрируете со строгого SPA, такого как Create React App или Vite, у вас может быть существующий код, который поверхностно маршрутизирует для обновления состояния URL. Это может быть полезно для ручных переходов между представлениями в вашем приложении без использования маршрутизации Next.js по файловой системе.
Next.js позволяет использовать нативные методы window.history.pushState
и window.history.replaceState
для обновления истории браузера без перезагрузки страницы.
Вызовы pushState
и replaceState
интегрируются в маршрутизатор Next.js, позволяя синхронизироваться с usePathname
и useSearchParams
.
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Сортировать по возрастанию</button>
<button onClick={() => updateSorting('desc')}>Сортировать по убыванию</button>
</>
)
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder) {
const urlSearchParams = new URLSearchParams(searchParams.toString())
urlSearchParams.set('sort', sortOrder)
window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>Сортировать по возрастанию</button>
<button onClick={() => updateSorting('desc')}>Сортировать по убыванию</button>
</>
)
}
Узнайте больше о том, как работают маршрутизация и навигация в Next.js.
Использование Server Actions в клиентских компонентах
Вы можете постепенно внедрять Server Actions, продолжая использовать клиентские компоненты. Это позволяет удалить шаблонный код для вызова API-маршрута и вместо этого использовать функции React, такие как useActionState
, для обработки состояний загрузки и ошибок.
Например, создайте вашу первую Server Action:
'use server'
export async function create() {}
'use server'
export async function create() {}
Вы можете импортировать и использовать Server Action из клиента, аналогично вызову функции JavaScript. Вам не нужно вручную создавать конечную точку API:
Узнайте больше о изменении данных с помощью Server Actions.
Статический экспорт (опционально)
Next.js также поддерживает генерацию полностью статического сайта. Это имеет некоторые преимущества перед строгими SPA:
- Автоматическое разделение кода: Вместо отправки одного
index.html
Next.js сгенерирует HTML-файл для каждого маршрута, так что ваши посетители получат контент быстрее, не дожидаясь загрузки клиентского JavaScript-бандла. - Улучшенный пользовательский опыт: Вместо минимального скелета для всех маршрутов вы получаете полностью отрендеренные страницы для каждого маршрута. Когда пользователи переходят на стороне клиента, переходы остаются мгновенными, как в SPA.
Чтобы включить статический экспорт, обновите вашу конфигурацию:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'export',
}
export default nextConfig
После выполнения next build
Next.js создаст папку out
с HTML/CSS/JS-ресурсами для вашего приложения.
Примечание: Серверные функции Next.js не поддерживаются при статическом экспорте. Узнайте больше.
Миграция существующих проектов на Next.js
Вы можете постепенно мигрировать на Next.js, следуя нашим руководствам:
Если вы уже используете SPA с Pages Router, вы можете узнать, как постепенно внедрить App Router.
Самостоятельное размещение
Узнайте, как самостоятельно разместить приложение Next.js на сервере Node.js, в Docker-образе или в виде статических HTML-файлов (статический экспорт).
Статический экспорт
Next.js позволяет начать со статического сайта или SPA (Single-Page Application), а затем при необходимости добавить серверные функции.