使用 Next.js 设置 Redux 工具包
¥Redux Toolkit Setup with Next.js
如何通过 Next.js 框架 设置和使用 Redux Toolkit
¥How to set up and use Redux Toolkit with the Next.js framework
熟悉 ES2015 语法和特性
¥Familiarity with ES2015 syntax and features
React 术语知识:JSX、状态、函数组件、属性 和 钩子
¥Knowledge of React terminology: JSX, State, Function Components, Props, and Hooks
对 Redux 术语和概念 的理解
¥Understanding of Redux terms and concepts
建议完成 快速入门教程 和 TypeScript 快速入门教程,最好还有完整的 Redux 要点 教程
¥Working through the Quick Start tutorial and TypeScript Quick Start tutorial is recommended, and ideally the full Redux Essentials tutorial as well
介绍
¥Introduction
Next.js 是一个流行的 React 服务器端渲染框架,它为正确使用 Redux 带来了一些独特的挑战。这些挑战包括:
¥Next.js is a popular server side rendering framework for React that presents some unique challenges for using Redux properly. These challenges include:
按请求安全 Redux 存储创建:Next.js 服务器可以同时处理多个请求。这意味着 Redux 存储应该根据请求创建,并且存储不应在请求之间共享。
¥Per-request safe Redux store creation: A Next.js server can handle multiple requests simultaneously. This means that the Redux store should be created per request and that the store should not be shared across requests.
SSR 友好的存储水合作用:Next.js 应用渲染两次,第一次在服务器上,第二次在客户端上。无法在客户端和服务器上渲染相同的页面内容将导致 "水合错误"。因此,Redux 存储必须在服务器上初始化,然后使用相同的数据在客户端上重新初始化,以避免水合问题。
¥SSR-friendly store hydration: Next.js applications are rendered twice, first on the server and again on the client. Failure to render the same page contents on both the client and the server will result in a "hydration error". So the Redux store will have to be initialized on the server and then re-initialized on the client with the same data in order to avoid hydration issues.
SPA 路由支持:Next.js 支持客户端路由的混合模型。客户的首页加载将从服务器获取 SSR 结果。后续的页面导航将由客户端处理。这意味着,通过在布局中定义单例存储,需要在路由导航时有选择地重置特定于路由的数据,而非特定于路由的数据需要保留在存储中。
¥SPA routing support: Next.js supports a hybrid model for client side routing. A customer's first page load will get an SSR result from the server. Subsequent page navigation will be handled by the client. This means that with a singleton store defined in the layout, route-specific data will need to be selectively reset on route navigation, while non-route-specific data will need to be retained in the store.
服务器缓存友好:Next.js 的最新版本(特别是使用 App Router 架构的应用)支持积极的服务器缓存。理想的存储架构应该与这种缓存兼容。
¥Server caching friendly: Recent versions of Next.js (specifically applications using the App Router architecture) support aggressive server caching. The ideal store architecture should be compatible with this caching.
¥There are two architectures for a Next.js application: the Pages Router and the App Router.
Pages Router 是 Next.js 的原始架构。如果你使用的是 Pages Router,则 Redux 设置主要通过使用 next-redux-wrapper
库 来处理,该 next-redux-wrapper
库 将 Redux 存储与 Pages Router 数据获取方法(如 getServerSideProps
)集成在一起。
¥The Pages Router is the original architecture for Next.js. If you're using the Pages Router, Redux setup is primarily handled by using the next-redux-wrapper
library, which integrates a Redux store with the Pages Router data fetching methods like getServerSideProps
.
本指南将重点介绍 App Router 架构,因为它是 Next.js 的新默认架构选项。
¥This guide will focus on the App Router architecture, as it is the new default architecture option for Next.js.
如何阅读本指南
¥How to Read This Guide
此页面假设你已经拥有基于 App Router 架构的现有 Next.js 应用。
¥This page assumes that you already have an existing Next.js application based on the App Router architecture.
如果你想继续,你可以使用 npx create-next-app my-app
创建一个新的空 Next 项目 - 默认提示将设置一个启用了 App Router 的新项目。然后,添加 @reduxjs/toolkit
和 react-redux
作为依赖。
¥If you want to follow along, you can create a new empty Next project with npx create-next-app my-app
- the default prompts will set up a new project with the App Router enabled. Then, add @reduxjs/toolkit
and react-redux
as dependencies.
你还可以使用 npx create-next-app --example with-redux my-app
创建一个新的 Next+Redux 项目,其中包括本页中描述的初始设置部分。
¥You can also create a new Next+Redux project with npx create-next-app --example with-redux my-app
, which includes the initial setup pieces described in this page.
App Router 架构和 Redux
¥The App Router Architecture and Redux
Next.js App Router 的主要新功能是增加了对 React 服务器组件 (RSC) 的支持。RSC 是一种特殊类型的 React 组件,仅在服务器上渲染,而不是在客户端和服务器上渲染的 "client" 组件。RSC 可以定义为 async
函数,并在渲染期间返回 promise,因为它们对要渲染的数据发出异步请求。
¥The primary new feature of the Next.js App Router is the addition of support for React Server Components (RSCs). RSCs are a special type of React component that only renders on the server, as opposed to "client" components that render on both the client and the server. RSCs can be defined as async
functions and return promises during rendering as they make async requests for data to render.
RSC 阻止数据请求的能力意味着使用 App Router,你不再需要 getServerSideProps
来获取数据进行渲染。树中的任何组件都可以发出异步数据请求。虽然这非常方便,但这也意味着如果你定义全局变量(如 Redux 存储),它们将在请求之间共享。这是一个问题,因为 Redux 存储可能会受到其他请求的数据的污染。
¥RSCs ability to block for data requests means that with the App Router you no longer have getServerSideProps
to fetch data for rendering. Any component in the tree can make asynchronous requests for data. While this is very convenient it also means thats if you define global variables (like the Redux store) they will be shared across requests. This is a problem because the Redux store could be contaminated with data from other requests.
根据 App Router 的架构,我们对适当使用 Redux 提出了以下一般建议:
¥Based on the architecture of the App Router we have these general recommendations for appropriate use of Redux:
没有全局存储 - 由于 Redux 存储在请求之间共享,因此不应将其定义为全局变量。相反,应该根据请求创建存储。
¥No global stores - Because the Redux store is shared across requests, it should not be defined as a global variable. Instead, the store should be created per request.
RSC 不应读取或写入 Redux 存储 - RSC 不能使用钩子或上下文。它们并不意味着是有状态的。让 RSC 从全局存储中读取或写入值违反了 Next.js App Router 的架构。
¥RSCs should not read or write the Redux store - RSCs cannot use hooks or context. They aren't meant to be stateful. Having an RSC read or write values from a global store violates the architecture of the Next.js App Router.
存储应该只包含可变数据 - 我们建议你谨慎使用 Redux 来处理全局且可变的数据。
¥The store should only contain mutable data - We recommend that you use your Redux sparingly for data intended to be global and mutable.
这些建议特定于使用 Next.js App Router 编写的应用。单页应用 (SPA) 不在服务器上执行,因此可以将存储定义为全局变量。SPA 无需担心 RSC,因为 SPA 中不存在 RSC。单例存储可以存储你想要的任何数据。
¥These recommendations are specific to applications written with the Next.js App Router. Single Page Applications (SPAs) don't execute on the server and therefore can define stores as global variables. SPAs don't need to worry about RSCs since they don't exist in SPAs. And singleton stores can store whatever data you want.
文件夹结构
¥Folder Structure
接下来可以创建将 /app
文件夹放在根目录下或嵌套在 /src/app
下的应用。你的 Redux 逻辑应该位于 /app
文件夹旁边的单独文件夹中。通常将 Redux 逻辑放在名为 /lib
的文件夹中,但这不是必需的。
¥Next apps can be created to have the /app
folder either at the root, or nested under /src/app
. Your Redux logic should go in a separate folder, alongside the /app
folder. It's common to put the Redux logic in a folder named /lib
, but not required.
/lib
文件夹内的文件和文件夹结构由你决定,但我们通常建议使用 基于 "功能文件夹" 的结构 作为 Redux 逻辑。
¥The file and folder structure inside of that /lib
folder is up to you, but we generally recommend a "feature folder"-based structure for the Redux logic.
一个典型的例子可能如下所示:
¥A typical example might look like:
/app
layout.tsx
page.tsx
StoreProvider.tsx
/lib
store.ts
/features
/todos
todosSlice.ts
我们将在本指南中使用这种方法。
¥We'll use that approach for this guide.
初始设置
¥Initial Setup
与 RTK TypeScript 教程 类似,我们需要为 Redux 存储创建一个文件,以及推断的 RootState
和 AppDispatch
类型。
¥Similar to the the RTK TypeScript Tutorial, we need to create a file for the Redux store, as well as the inferred RootState
and AppDispatch
types.
但是,Next 的多页面架构需要与单页面应用设置有所不同。
¥However, Next's multi-page architecture requires some differences from that single-page app setup.
根据请求创建 Redux Store
¥Creating a Redux Store per Request
第一个更改是从将 store
定义为全局变量或模块单例变量,改为定义为每个请求返回新存储的 makeStore
函数:
¥The first change is to move from defining store
as a global or module-singleton variable, to defining a makeStore
function that returns a new store for each request:
- 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
,我们可以使用它为每个请求创建一个存储实例,同时保留 Redux Toolkit 提供的强类型安全性(如果你选择使用 TypeScript)。
¥Now we have a function, makeStore
, that we can use to create a store instance per-request while retaining the strong type safety (if you choose to use TypeScript) that Redux Toolkit provides.
我们没有导出 store
变量,但我们可以从 makeStore
的返回类型推断出 RootState
和 AppDispatch
类型。
¥We don't have a store
variable exported, but we can infer the RootState
and AppDispatch
types from the return type of makeStore
.
你还需要创建并导出 以及 React-Redux hooks 的预输入版本,以简化以后的使用:
¥You'll also want to create and export pre-typed versions of the React-Redux hooks as well, to simplify usage later:
- 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()
提供存储
¥Providing the Store
要使用这个新的 makeStore
函数,我们需要创建一个新的 "client" 组件,该组件将创建存储并使用 React-Redux Provider
组件共享它。
¥To use this new makeStore
function we need to create a new "client" component that will create the store and share it using the React-Redux Provider
component.
- 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>()
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()
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
在此示例代码中,我们通过检查引用的值来确保该客户端组件重新渲染安全,以确保存储仅创建一次。对于服务器上的每个请求,此组件只会渲染一次,但如果树中此组件上方有有状态客户端组件,或者此组件还包含其他导致错误的可变状态,则可能会在客户端上重新渲染多次。 重新渲染。
¥In this example code we are ensuring that this client component is re-render safe by checking the value of the reference to ensure that the store is only created once. This component will only be rendered once per request on the server, but might be re-rendered multiple times on the client if there are stateful client components located above this component in the tree, or if this component also contains other mutable state that causes a re-render.
任何与 Redux 存储交互(创建、提供、读取或写入)的组件都需要是客户端组件。这是因为访问存储需要 React 上下文,而上下文仅在客户端组件中可用。
¥Any component that interacts with the Redux store (creating it, providing it, reading from it, or writing to it) needs to be a client component. This is because accessing the store requires React context, and context is only available in client components.
下一步是将 StoreProvider
包含在使用存储的树上方的任何位置。如果使用该布局的所有路由都需要该存储,则可以在布局组件中找到该存储。或者,如果存储仅在特定路由中使用,你可以在该路由处理程序中创建并提供存储。在树的所有客户端组件中,你可以像通常使用 react-redux
提供的钩子一样使用存储。
¥The next step is to include the StoreProvider
anywhere in the tree above where the store is used. You can locate the store in the layout component if all the routes using that layout need the store. Or if the store is only used in a specific route you can create and provide the store in that route handler. In all client components further down the tree, you can use the store exactly as you would normally using the hooks provided by react-redux
.
加载初始数据
¥Loading Initial Data
如果你需要使用父组件中的数据初始化存储,请将该数据定义为客户端 StoreProvider
组件上的 prop,并在切片上使用 Redux 操作来设置存储中的数据,如下所示。
¥If you need to initialize the store with data from the parent component, then define that data as a prop on the client StoreProvider
component and use a Redux action on the slice to set the data in the store as shown below.
- 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>
}
附加配置
¥Additional Configuration
每条路由的状态
¥Per-route state
如果你通过 next/navigation
使用 Next.js 对客户端 SPA 式导航的支持,那么当客户从一个页面导航到另一个页面时,只会重新渲染路由组件。这意味着,如果你在布局组件中创建并提供了 Redux 存储,它将在路由更改时保留。如果你仅将存储用于全局可变数据,那么这不是问题。但是,如果你使用存储来存储每条路由的数据,那么当路由发生变化时,你将需要重置存储中特定于路由的数据。
¥If you use Next.js's support for client side SPA-style navigation by using next/navigation
, then when customers navigate from page to page only the route component will be re-rendered. This means that if you have a Redux store created and provided in the layout component it will be preserved across route changes. This is not a problem if you are only using the store for global, mutable data. However, if you are using the store for per-route data then you will need to reset the route-specific data in the store when the route changes.
下面显示的是一个 ProductName
示例组件,它使用 Redux 存储来管理产品的可变名称。产品详细信息路由的 ProductName
组成部分。为了确保我们在存储中拥有正确的名称,我们需要在 ProductName
组件首次渲染时在存储中设置值,这发生在产品详细信息路由的任何路由更改上。
¥Shown below is a ProductName
example component that uses the Redux store to manage the mutable name of a product. The ProductName
component part of a product detail route. In order to ensure that we have the correct name in the store we need to set the value in the store any time the ProductName
component is initially rendered, which happens on any route change to the product detail route.
- 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
ref 用于确保每次路由更改时仅初始化存储一次。
¥Here we are using the same initialization pattern as before, of dispatching actions to the store, to set the route-specific data. The initialized
ref is used to ensure that the store is only initialized once per route change.
值得注意的是,用 useEffect
初始化存储是行不通的,因为 useEffect
只在客户端运行。这会导致水合错误或闪烁,因为服务器端渲染的结果与客户端渲染的结果不匹配。
¥It is worth noting that initializing the store with a useEffect
would not work because useEffect
only runs on the client. This would result in hydration errors or flicker because the result from a server side render would not match the result from the client side render.
缓存
¥Caching
App Router 有四个独立的缓存,包括 fetch
请求和路由缓存。最有可能导致问题的缓存是路由缓存。如果你有一个接受登录的应用,你可能拥有根据用户渲染不同数据的路由(例如主路由,/
),你将需要使用 dynamic
从路由处理程序导出 禁用路由缓存:
¥The App Router has four separate caches including fetch
request and route caches. The most likely cache to cause issues is the route cache. If you have an application that accepts login you may have routes (e.g. the home route, /
) that render different data based on the user you will need to disable the route cache by using the dynamic
export from the route handler:
- TypeScript
- JavaScript
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
突变后,你还应该根据需要调用 revalidatePath
或 revalidateTag
来使缓存无效。
¥After a mutation you should also invalidate the cache by calling revalidatePath
or revalidateTag
as appropriate.
RTK 查询
¥RTK Query
我们建议仅在客户端上使用 RTK 查询来获取数据。服务器上的数据获取应使用来自 async
RSC 的 fetch
请求。
¥We recommend using RTK Query for data fetching on the client only. Data fetching on the server should use fetch
requests from async
RSCs.
你可以在 Redux 工具包查询教程 中了解有关 Redux Toolkit Query 的更多信息。
¥You can learn more about Redux Toolkit Query in the Redux Toolkit Query tutorial.
将来,RTK Query 可能能够通过 React Server 组件接收从服务器获取的数据,但这是未来的功能,需要对 React 和 RTK Query 进行更改。
¥In the future, RTK Query may be able to receive data fetched on the server via React Server Components, but that is a future capability that will require changes to both React and RTK Query.
检查你的工作
¥Checking Your Work
你应该检查三个关键区域,以确保正确设置 Redux Toolkit:
¥There are three key areas that you should check to ensure that you have set up Redux Toolkit correctly:
服务器端渲染 - 检查服务器的 HTML 输出,以确保 Redux 存储中的数据存在于服务器端渲染的输出中。
¥Server Side Rendering - Check the HTML output of the server to ensure that the data in the Redux store is present in the server side rendered output.
路由变更 - 在同一路由上的页面之间以及不同路由之间导航,以确保正确初始化特定于路由的数据。
¥Route Change - Navigate between pages on the same route as well as between different routes to ensure that route-specific data is initialized properly.
突变 - 通过执行突变,然后导航离开路由并返回到原始路由,检查存储是否与 Next.js App Router 缓存兼容,以确保数据已更新。
¥Mutations - Check that the store is compatible with the Next.js App Router caches by performing a mutation and then navigating away from the route and back to the original route to ensure that the data is updated.
总体建议
¥Overall Recommendations
App Router 为 React 应用提供了与 Pages Router 或 SPA 应用截然不同的架构。我们建议根据这种新架构重新考虑你的状态管理方法。在 SPA 应用中,拥有一个包含驱动应用所需的所有数据(可变和不可变)的大型存储并不罕见。对于 App Router 应用,我们建议你应该:
¥The App Router presents a dramatically different architecture for React applications from either the Pages Router or a SPA application. We recommend rethinking your approach to state management in the light of this new architecture. In SPA applications it's not unusual to have a large store that contains all the data, both mutable and immutable, required to drive the application. For App Router applications we recommend that you should:
仅将 Redux 用于全局共享的可变数据
¥only use Redux for globally shared, mutable data
使用 Next.js 状态(搜索参数、路由参数、表单状态等)、React 上下文和 React hook 的组合来进行所有其他状态管理。
¥use a combination of Next.js state (search params, route parameters, form state, etc.), React context and React hooks for all other state management.
你学到了什么
¥What You've Learned
这是如何通过 App Router 设置和使用 Redux Toolkit 的简要概述:
¥That was a brief overview of how to set up and use Redux Toolkit with the App Router:
使用
makeStore
函数中的configureStore
为每个请求创建一个 Redux 存储¥Create a Redux store per request by using
configureStore
wrapped in amakeStore
function使用 "client" 组件向 React 应用组件提供 Redux 存储
¥Provide the Redux store to the React application components using a "client" component
仅与客户端组件中的 Redux 存储交互,因为只有客户端组件才能访问 React 上下文
¥Only interact with the Redux store in client components because only client components have access to React context
像平常使用 React-Redux 中提供的钩子一样使用存储
¥Use the store as you normally would using the hooks provided in React-Redux
你需要考虑布局中的全局存储中具有每条路由状态的情况
¥You need to account for the case where you have per-route state in a global store located in the layout
下一步是什么?
¥What's Next?
我们建议你阅读 Redux 核心文档中的 "Redux 要点" 和 "Redux 基础知识" 教程,这将使你全面了解 Redux 的工作原理、Redux Toolkit 的作用以及如何正确使用它。
¥We recommend going through the "Redux Essentials" and "Redux Fundamentals" tutorials in the Redux core docs, which will give you a complete understanding of how Redux works, what Redux Toolkit does, and how to use it correctly.