Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
Настройка Redux Toolkit с Next.js
- Как настроить и использовать Redux Toolkit с фреймворком Next.js
- Знакомство с синтаксисом и возможностями ES2015
- Знание терминологии React: JSX, Состояние, Функциональные компоненты, Пропсы и Хуки
- Понимание терминов и концепций Redux
- Рекомендуется пройти краткое руководство и краткое руководство по TypeScript, а в идеале и полное руководство Redux Essentials
Введение
Next.js — популярный фреймворк для серверного рендеринга React, который создаёт уникальные сложности для корректного использования Redux. Эти сложности включают:
-
Безопасное создание хранилища Redux для каждого запроса: Сервер Next.js может обрабатывать несколько запросов одновременно. Это означает, что хранилище Redux должно создаваться для каждого запроса и не должно использоваться совместно между запросами.
-
Гидратация хранилища, дружественная к SSR: Приложения Next.js рендерятся дважды — сначала на сервере, затем на клиенте. Несовпадение содержимого страницы на сервере и клиенте вызовет "ошибку гидратации". Поэтому хранилище Redux должно инициализироваться на сервере и повторно инициализироваться на клиенте с теми же данными для предотвращения проблем с гидратацией.
-
Поддержка маршрутизации SPA: Next.js использует гибридную модель клиентской маршрутизации. Первая загрузка страницы пользователем выполняется через SSR на сервере. Последующая навигация обрабатывается на клиенте. Это означает, что при использовании единого хранилища в макете, данные, специфичные для маршрута, должны выборочно сбрасываться при навигации, тогда как данные, не относящиеся к маршруту, должны сохраняться в хранилище.
-
Совместимость с серверным кэшированием: Новые версии Next.js (особенно приложения с архитектурой App Router) поддерживают агрессивное серверное кэширование. Идеальная архитектура хранилища должна быть совместима с этим кэшированием.
Существует две архитектуры для приложения Next.js: Pages Router и App Router.
Pages Router — оригинальная архитектура Next.js. При использовании Pages Router настройка Redux выполняется преимущественно через библиотеку next-redux-wrapper, которая интегрирует хранилище Redux с методами получения данных Pages Router, такими как getServerSideProps.
В этом руководстве основное внимание уделяется архитектуре App Router, так как это новая архитектура, выбранная по умолчанию для Next.js.
Как читать это руководство
Эта страница предполагает, что у вас уже есть существующее приложение Next.js, основанное на архитектуре App Router.
Чтобы следовать инструкциям, создайте новый пустой проект Next командой npx create-next-app my-app — стандартные подсказки настроят проект с включённым App Router. Затем добавьте зависимости @reduxjs/toolkit и react-redux.
Вы также можете создать проект Next+Redux командой npx create-next-app --example with-redux my-app, которая включает начальные настройки, описанные на этой странице.
Архитектура App Router и Redux
Ключевой новой особенностью App Router в Next.js стало добавление поддержки React Server Components (RSC). RSC — это особый тип React-компонентов, которые рендерятся только на сервере, в отличие от "клиентских" компонентов, выполняющихся и на клиенте, и на сервере. RSC могут быть определены как async-функции и возвращать промисы во время рендеринга, выполняя асинхронные запросы данных.
Возможность RSC блокировать выполнение для запросов данных означает, что в App Router больше не нужен getServerSideProps для получения данных перед рендерингом. Любой компонент в дереве может делать асинхронные запросы данных. Хотя это очень удобно, это также означает, что глобальные переменные (например, хранилище Redux) будут общими для всех запросов. Это создаёт проблему: хранилище Redux может быть загрязнено данными из других запросов.
Исходя из архитектуры App Router, мы даём следующие общие рекомендации по использованию Redux:
-
Не используйте глобальные хранилища — Поскольку хранилище Redux общее для всех запросов, его нельзя определять как глобальную переменную. Вместо этого создавайте хранилище для каждого запроса.
-
RSC не должны читать или изменять хранилище Redux — RSC не могут использовать хуки или контекст. Они не предназначены для хранения состояния. Чтение или запись значений из глобального хранилища нарушает архитектуру App Router в Next.js.
-
Хранилище должно содержать только изменяемые данные — Рекомендуем использовать Redux экономно, только для глобальных изменяемых данных.
Эти рекомендации специфичны для приложений на Next.js App Router. Одностраничные приложения (SPA) не выполняются на сервере, поэтому могут определять хранилища как глобальные переменные. SPA не работают с RSC, а в синглтон-хранилищах можно хранить любые данные.
Структура папок
Приложения Next.js могут иметь папку /app либо в корне, либо внутри /src/app. Логику Redux следует размещать в отдельной папке рядом с /app. Обычно её называют /lib, но это не обязательно.
Структура файлов внутри /lib — на ваше усмотрение, но мы рекомендуем структуру на основе "feature folders" для логики Redux.
Типичный пример может выглядеть следующим образом:
/app
layout.tsx
page.tsx
StoreProvider.tsx
/lib
store.ts
/features
/todos
todosSlice.ts
В этом руководстве мы будем использовать такой подход.
Начальная настройка
Как и в Руководстве по TypeScript для RTK, нам нужно создать файл для хранилища Redux, а также вывести типы RootState и AppDispatch.
Однако многостраничная архитектура Next.js требует некоторых отличий от настройки одностраничного приложения.
Создание хранилища Redux для каждого запроса
Первое изменение: вместо объявления store как глобальной или модульной переменной-синглтона мы создаём функцию makeStore, которая возвращает новое хранилище для каждого запроса:
- TypeScript
- JavaScript
import { configureStore } from '@reduxjs/toolkit'
export const makeStore = () => {
return configureStore({
reducer: {}
})
}
// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
import { configureStore } from '@reduxjs/toolkit'
export const makeStore = () => {
return configureStore({
reducer: {}
})
}
Теперь у нас есть функция makeStore для создания экземпляра хранилища на запрос, сохраняя строгую типизацию (если вы используете TypeScript), которую обеспечивает Redux Toolkit.
Мы не экспортируем переменную store, но можем вывести типы RootState и AppDispatch из возвращаемого типа makeStore.
Также создайте и экспортируйте предварительно типизированные версии хуков React-Redux для упрощения дальнейшего использования:
- TypeScript
- JavaScript
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()
import { useDispatch, useSelector, useStore } from 'react-redux'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes()
export const useAppSelector = useSelector.withTypes()
export const useAppStore = useStore.withTypes()
Предоставление хранилища
Чтобы использовать функцию makeStore, создадим новый "клиентский" компонент, который будет инициализировать хранилище и предоставлять его через провайдер React-Redux Provider.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
export default function StoreProvider({
children
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore | null>(null)
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export default function StoreProvider({ children }) {
const storeRef = useRef(null)
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
В этом примере кода мы гарантируем безопасность повторного рендеринга для клиентского компонента, проверяя значение ссылки, чтобы хранилище создавалось только один раз. На сервере этот компонент будет отрендерен единожды за запрос, но на клиенте он может перерендериваться многократно — если выше по дереву находятся другие клиентские компоненты с состоянием или если сам компонент содержит изменяемое состояние, вызывающее перерендеринг.
Любой компонент, взаимодействующий с хранилищем Redux (создающий, предоставляющий, читающий или изменяющий данные), должен быть клиентским. Это потому, что доступ к хранилищу требует контекста React, который доступен только в клиентских компонентах.
Следующий шаг — разместить StoreProvider где-либо выше в дереве компонентов, чем место использования хранилища. Вы можете добавить его в компонент макета (layout), если все маршруты с этим макетом используют хранилище. Если же хранилище нужно только для конкретного маршрута — создайте и предоставьте его в обработчике этого маршрута. Во всех клиентских компонентах ниже по дереву вы сможете использовать хранилище стандартными методами через хуки react-redux.
Загрузка начальных данных
Если вам нужно инициализировать хранилище данными из родительского компонента, определите эти данные как пропс клиентского компонента StoreProvider и используйте действие Redux в слайсе, чтобы установить данные в хранилище, как показано ниже.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'
export default function StoreProvider({
count,
children
}: {
count: number
children: React.ReactNode
}) {
const storeRef = useRef<AppStore | null>(null)
if (!storeRef.current) {
storeRef.current = makeStore()
storeRef.current.dispatch(initializeCount(count))
}
return <Provider store={storeRef.current}>{children}</Provider>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'
export default function StoreProvider({ count, children }) {
const storeRef = useRef(null)
if (!storeRef.current) {
storeRef.current = makeStore()
storeRef.current.dispatch(initializeCount(count))
}
return <Provider store={storeRef.current}>{children}</Provider>
}
Дополнительная настройка
Состояние для каждого маршрута
Если вы используете в Next.js клиентскую навигацию в стиле SPA через next/navigation, то при переходе между страницами перерендеривается только компонент маршрута. Это означает, что если хранилище Redux создано и предоставлено в компоненте макета, оно сохранится при смене маршрутов. Это не проблема, если хранилище используется только для глобальных изменяемых данных. Однако если в нём хранятся данные для конкретного маршрута — вам потребуется сбрасывать их при переходе.
Ниже показан пример компонента ProductName, использующего хранилище Redux для управления изменяемым названием товара. ProductName является частью маршрута детализации товара. Чтобы гарантировать корректное название в хранилище, мы устанавливаем значение при каждом первоначальном рендеринге компонента ProductName, что происходит при любом переходе на маршрут детализации товара.
- TypeScript
- JavaScript
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName,
Product
} from '../lib/features/product/productSlice'
export default function ProductName({ product }: { product: Product }) {
// Initialize the store with the product information
const store = useAppStore()
const initialized = useRef(false)
if (!initialized.current) {
store.dispatch(initializeProduct(product))
initialized.current = true
}
const name = useAppSelector(state => state.product.name)
const dispatch = useAppDispatch()
return (
<input
value={name}
onChange={e => dispatch(setProductName(e.target.value))}
/>
)
}
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName
} from '../lib/features/product/productSlice'
export default function ProductName({ product }) {
// Initialize the store with the product information
const store = useAppStore()
const initialized = useRef(false)
if (!initialized.current) {
store.dispatch(initializeProduct(product))
initialized.current = true
}
const name = useAppSelector(state => state.product.name)
const dispatch = useAppDispatch()
return (
<input
value={name}
onChange={e => dispatch(setProductName(e.target.value))}
/>
)
}
Здесь используется та же схема инициализации, что и ранее: диспетчеризация действий в хранилище для установки данных маршрута. Ссылка initialized гарантирует, что хранилище инициализируется только раз за смену маршрута.
Важно: инициализация через useEffect не сработает, потому что useEffect выполняется только на клиенте. Это вызовет ошибки гидратации или "мерцание", поскольку результат серверного рендеринга не совпадёт с клиентским.
Кэширование
В App Router есть четыре отдельных кэша, включая кэши запросов fetch и маршрутов. Наиболее вероятные проблемы создаст кэш маршрутов. Если в вашем приложении есть авторизация и маршруты (например, главная /), отображающие разные данные для разных пользователей — отключите кэш маршрутов через dynamic в обработчике маршрута:
- TypeScript
- JavaScript
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
После мутации также следует инвалидировать кэш вызовом revalidatePath или revalidateTag.
RTK Query
Мы рекомендуем использовать RTK Query для получения данных только на клиенте. Для серверного получения данных используйте запросы fetch из async RSCs.
Подробнее о RTK Query читайте в официальном руководстве.
В будущем RTK Query сможет получать данные с сервера через React Server Components, но эта функциональность потребует изменений как в React, так и в самой RTK Query.
Проверка реализации
Следует проверить три ключевых аспекта, чтобы убедиться в корректной настройке Redux Toolkit:
-
Рендеринг на стороне сервера — проверьте HTML-вывод сервера, чтобы убедиться, что данные из хранилища Redux присутствуют в SSR-результате.
-
Смена маршрутов — переходите между страницами в пределах одного маршрута и между разными маршрутами, чтобы убедиться в правильной инициализации специфичных для маршрута данных.
-
Изменения состояния — проверьте совместимость хранилища с кэшами App Router Next.js: внесите изменения, перейдите на другой маршрут и вернитесь обратно, чтобы убедиться в обновлении данных.
Общие рекомендации
App Router представляет принципиально иную архитектуру для React-приложений по сравнению с Pages Router или SPA. Рекомендуем переосмыслить подход к управлению состоянием в свете этой архитектуры. В SPA-приложениях обычно используется крупное хранилище, содержащее все данные приложения. Для App Router рекомендуем:
-
использовать Redux только для глобальных изменяемых данных
-
использовать комбинацию состояний Next.js (параметры поиска, параметры маршрутов, состояние форм), контекста React и хуков для всех остальных задач управления состоянием.
Итоги изученного
Вы получили общее представление о настройке и использовании Redux Toolkit с App Router:
- Создавайте хранилище Redux для каждого запроса, используя
configureStoreвнутри функцииmakeStore - Предоставляйте хранилище React-компонентам через "клиентский" компонент
- Взаимодействуйте с хранилищем только в клиентских компонентах, так как только они имеют доступ к контексту React
- Используйте хранилище стандартными методами через хуки React-Redux
- Учитывайте особенности хранения маршрут-специфичных состояний в глобальном хранилище макета
Что дальше?
Рекомендуем изучить туториалы "Redux Essentials" и "Redux Fundamentals" в основной документации Redux, которые дадут полное понимание работы Redux, функциональности Redux Toolkit и правил его использования.