本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
Redux Toolkit 在 Next.js 中的配置
- 如何通过 Next.js 框架 配置和使用 Redux Toolkit
- 熟悉 ES2015 语法特性
- 了解 React 核心概念:JSX、状态管理、函数组件与属性 以及 Hooks
- 理解 Redux 术语与核心概念
- 建议完成 快速入门教程 和 TypeScript 快速入门,最好能完整学习 Redux 核心教程
简介
Next.js 是流行的 React 服务端渲染框架,在使用 Redux 时会面临以下特有挑战:
-
请求级安全的 Redux store 创建:Next.js 服务器可同时处理多个请求。这意味着每个请求都应创建独立的 Redux store,且 store 不能在请求间共享。
-
兼容 SSR 的 store 注水:Next.js 应用会分别在服务端和客户端各渲染一次。若两端渲染内容不一致会导致"注水错误"。因此必须在服务端初始化 Redux store,并在客户端用相同数据重新初始化,以避免注水问题。
-
SPA 路由支持:Next.js 采用混合路由模式。用户首次加载页面获得服务端渲染(SSR)结果,后续页面导航由客户端处理。这意味着在布局中定义的全局 store 需满足:路由切换时选择性重置路由相关数据,同时保留非路由相关数据。
-
兼容服务端缓存:新版 Next.js(特别是采用 App Router 架构的应用)支持激进的服务端缓存。理想的 store 架构应兼容这种缓存机制。
Next.js 应用有两种架构模式:Pages Router 和 App Router。
Pages Router 是 Next.js 的原始架构。若使用此模式,可通过 next-redux-wrapper 库配置 Redux,该库将 Redux store 与 Pages Router 的数据获取方法(如 getServerSideProps)集成。
本指南将聚焦于 App Router 架构,因其已成为 Next.js 的新默认架构选项。
阅读指南
本文假设你已存在基于 App Router 架构的 Next.js 应用。
若要跟随操作,可通过 npx create-next-app my-app 创建新项目——默认选项将启用 App Router。然后添加 @reduxjs/toolkit 和 react-redux 依赖。
也可通过 npx create-next-app --example with-redux my-app 创建包含初始配置的 Next+Redux 项目。
App Router 架构与 Redux
Next.js App Router 的主要新特性是增加了对 React 服务器组件(RSCs)的支持。RSCs 是一种特殊的 React 组件,仅在服务器端渲染,与在客户端和服务器端都会渲染的"客户端"组件不同。RSCs 可定义为 async 函数,在渲染过程中返回 Promise 以进行异步数据请求。
RSCs 能够阻塞数据请求的特性意味着,在 App Router 架构下不再需要 getServerSideProps 来获取渲染所需数据。组件树中的任何组件都可以发起异步数据请求。虽然这非常方便,但也意味着如果定义了全局变量(如 Redux store),它们将在多个请求间共享。这会导致 Redux store 可能被其他请求的数据污染。
基于 App Router 的架构,我们对 Redux 的合理使用提出以下建议:
-
避免全局 store - 由于 Redux store 会在多个请求间共享,不应将其定义为全局变量。应为每个请求创建独立的 store
-
RSCs 不应读写 Redux store - RSCs 无法使用钩子或上下文,其设计初衷是无状态的。让 RSC 从全局 store 读写值违反了 Next.js App Router 的架构原则
-
store 应仅包含可变数据 - 建议谨慎使用 Redux,仅存储需要全局访问且可变的数据
这些建议仅适用于采用 Next.js App Router 架构的应用。单页应用(SPAs)不在服务器端执行,因此可将 store 定义为全局变量。SPAs 无需考虑 RSCs,单例 store 可存储任意数据。
目录结构
Next 应用可在根目录创建 /app 文件夹,或在 /src/app 下创建。Redux 逻辑应放在与 /app 并列的独立目录中,通常命名为 /lib(非强制要求)。
/lib 目录内的文件结构由您决定,但我们通常建议采用基于“功能文件夹”的结构组织 Redux 逻辑。
典型示例如下:
/app
layout.tsx
page.tsx
StoreProvider.tsx
/lib
store.ts
/features
/todos
todosSlice.ts
本指南将采用此方法。
初始设置
与 RTK TypeScript 教程 类似,我们需要创建 Redux store 文件以及推断的 RootState 和 AppDispatch 类型。
但 Next 的多页面架构需要与单页应用设置有所不同。
为每个请求创建 Redux store
首要变化是将 store 的定义从全局/模块单例变量改为创建 makeStore 函数,该函数为每个请求返回新的 store:
- 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 函数,可为每个请求创建 store 实例,同时保留 Redux Toolkit 提供的强类型安全(若使用 TypeScript)。
虽然不再导出 store 变量,但我们可以根据 makeStore 的返回类型推断出 RootState 和 AppDispatch 类型。
您还需创建并导出预定义类型的 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()
提供 Store
要使用这个新的 makeStore 函数,我们需要创建一个新的"客户端"组件,该组件将创建 store 并通过 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>
}
在此示例代码中,我们通过检查引用值确保这个客户端组件在重新渲染时是安全的,从而保证 store 仅被创建一次。该组件在服务器端每个请求仅渲染一次,但在客户端,如果组件树中存在位于其上方的有状态客户端组件,或者该组件自身包含导致重新渲染的可变状态,则可能会被多次渲染。
任何与 Redux store 交互的组件(创建、提供、读取或写入)都必须是客户端组件。这是因为访问 store 需要 React 上下文(context),而上下文仅适用于客户端组件。
下一步是在 store 使用位置之上的组件树中任意位置包含 StoreProvider。如果使用该布局的所有路由都需要 store,可以将其放在布局组件中;如果 store 仅用于特定路由,则可在该路由处理程序中创建并提供。在下层所有客户端组件中,可以像常规方式一样使用 react-redux 提供的钩子操作 store。
加载初始数据
如果需要使用父组件数据初始化 store,请将这些数据定义为客户端 StoreProvider 组件的 prop,并通过如下所示的分片(slice)Redux action 将数据设置到 store 中。
- 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/navigation 实现 Next.js 的客户端 SPA 风格导航,用户切换页面时仅路由组件会重新渲染。这意味着布局组件中创建的 Redux store 将在路由变更时保留。若 store 仅用于全局可变数据没有问题,但若存储每路由数据,则需要在路由变更时重置这些特定数据。
下面的 ProductName 示例组件使用 Redux store 管理产品的可变名称,ProductName 组件属于产品详情路由。为确保 store 中名称正确,我们需要在每次 ProductName 组件初始渲染时(即每次切换到产品详情路由时)设置 store 中的值。
- 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))}
/>
)
}
这里使用与之前相同的初始化模式:向 store 分发 action 来设置路由特定数据。initialized 引用(ref)确保每次路由变更时 store 仅初始化一次。
值得注意的是,使用 useEffect 初始化 store 不可行,因为 useEffect 仅在客户端运行。这会导致水合错误或页面闪烁,因为服务器端渲染结果与客户端渲染结果不匹配。
缓存
App Router 包含四种独立缓存(含 fetch 请求缓存和路由缓存)。最易引发问题的是路由缓存。如果应用需要登录(例如首页路由 / 根据用户渲染不同数据),需通过路由处理程序的 dynamic 导出禁用路由缓存:
- TypeScript
- JavaScript
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
数据变更后,还应视情况调用 revalidatePath 或 revalidateTag 使缓存失效。
RTK Query
我们建议仅在客户端使用 RTK Query 获取数据。服务器端数据获取应使用 async RSC 中的 fetch 请求。
可在 Redux Toolkit Query 教程中了解更多 RTK Query 相关内容。
未来 RTK Query 可能通过 React Server Components 接收服务端获取的数据,但这需要 React 和 RTK Query 双方进行改动才能实现。
验证设置
为确保正确设置 Redux Toolkit,你需要检查以下三个关键方面:
-
服务端渲染 - 检查服务器的 HTML 输出,确保 Redux store 中的数据出现在服务端渲染的输出中
-
路由变更 - 在相同路由和不同路由的页面间导航,确保路由特定的数据被正确初始化
-
数据变更 - 执行变更操作后离开路由再返回,验证 store 是否兼容 Next.js App Router 的缓存机制,确保数据已更新
总体建议
App Router 为 React 应用提供了与 Pages Router 或 SPA 应用截然不同的架构。我们建议基于此新架构重新思考状态管理方案。在 SPA 应用中,通常会使用包含所有数据(可变与不可变)的大型 store;对于 App Router 应用,我们建议:
-
仅将 Redux 用于全局共享的可变数据
-
对于其他状态管理,结合使用 Next.js 状态(搜索参数、路由参数、表单状态等)、React context 和 React hooks
学习要点
以上是使用 App Router 设置 Redux Toolkit 的简要指南:
- 通过
makeStore函数包裹configureStore,实现按请求创建 Redux store - 使用"客户端"组件将 Redux store 提供给 React 应用
- 仅在客户端组件中与 Redux store 交互(只有客户端组件能访问 React context)
- 使用 React-Redux 提供的 hooks 正常操作 store
- 需处理布局中全局 store 的按路由状态问题
下一步建议
建议学习 Redux 核心文档中的《Redux 精要》和《Redux 基础》教程,这将帮助你全面理解 Redux 工作原理、Redux Toolkit 功能及正确使用方法。