跳至主内容
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

Redux Toolkit 在 Next.js 中的配置

你将学习
前置知识

简介

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 RouterApp 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/toolkitreact-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 文件以及推断的 RootStateAppDispatch 类型。

但 Next 的多页面架构需要与单页应用设置有所不同。

为每个请求创建 Redux store

首要变化是将 store 的定义从全局/模块单例变量改为创建 makeStore 函数,该函数为每个请求返回新的 store:

lib/store.ts
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']

现在我们有了 makeStore 函数,可为每个请求创建 store 实例,同时保留 Redux Toolkit 提供的强类型安全(若使用 TypeScript)。

虽然不再导出 store 变量,但我们可以根据 makeStore 的返回类型推断出 RootStateAppDispatch 类型。

您还需创建并导出预定义类型的 React-Redux 钩子以简化后续使用:

lib/hooks.ts
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>()

提供 Store

要使用这个新的 makeStore 函数,我们需要创建一个新的"客户端"组件,该组件将创建 store 并通过 React-Redux 的 Provider 组件共享它。

app/StoreProvider.tsx
'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>
}

在此示例代码中,我们通过检查引用值确保这个客户端组件在重新渲染时是安全的,从而保证 store 仅被创建一次。该组件在服务器端每个请求仅渲染一次,但在客户端,如果组件树中存在位于其上方的有状态客户端组件,或者该组件自身包含导致重新渲染的可变状态,则可能会被多次渲染。

为什么需要客户端组件?

任何与 Redux store 交互的组件(创建、提供、读取或写入)都必须是客户端组件。这是因为访问 store 需要 React 上下文(context),而上下文仅适用于客户端组件。

下一步是在 store 使用位置之上的组件树中任意位置包含 StoreProvider。如果使用该布局的所有路由都需要 store,可以将其放在布局组件中;如果 store 仅用于特定路由,则可在该路由处理程序中创建并提供。在下层所有客户端组件中,可以像常规方式一样使用 react-redux 提供的钩子操作 store。

加载初始数据

如果需要使用父组件数据初始化 store,请将这些数据定义为客户端 StoreProvider 组件的 prop,并通过如下所示的分片(slice)Redux action 将数据设置到 store 中。

app/StoreProvider.tsx
'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>
}

额外配置

每路由状态

如果使用 next/navigation 实现 Next.js 的客户端 SPA 风格导航,用户切换页面时仅路由组件会重新渲染。这意味着布局组件中创建的 Redux store 将在路由变更时保留。若 store 仅用于全局可变数据没有问题,但若存储每路由数据,则需要在路由变更时重置这些特定数据。

下面的 ProductName 示例组件使用 Redux store 管理产品的可变名称,ProductName 组件属于产品详情路由。为确保 store 中名称正确,我们需要在每次 ProductName 组件初始渲染时(即每次切换到产品详情路由时)设置 store 中的值。

app/ProductName.tsx
'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))}
/>
)
}

这里使用与之前相同的初始化模式:向 store 分发 action 来设置路由特定数据。initialized 引用(ref)确保每次路由变更时 store 仅初始化一次。

值得注意的是,使用 useEffect 初始化 store 不可行,因为 useEffect 仅在客户端运行。这会导致水合错误或页面闪烁,因为服务器端渲染结果与客户端渲染结果不匹配。

缓存

App Router 包含四种独立缓存(含 fetch 请求缓存和路由缓存)。最易引发问题的是路由缓存。如果应用需要登录(例如首页路由 / 根据用户渲染不同数据),需通过路由处理程序的 dynamic 导出禁用路由缓存:

export const dynamic = 'force-dynamic'

数据变更后,还应视情况调用 revalidatePathrevalidateTag 使缓存失效。

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 功能及正确使用方法。