Skip to main content

使用 Next.js 设置 Redux 工具包

¥Redux Toolkit Setup with Next.js

你将学到什么
先决条件

介绍

¥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.

Next.js 应用有两种架构:页面路由应用路由

¥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/toolkitreact-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 存储创建一个文件,以及推断的 RootStateAppDispatch 类型。

¥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:

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,我们可以使用它为每个请求创建一个存储实例,同时保留 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 的返回类型推断出 RootStateAppDispatch 类型。

¥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:

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>()

提供存储

¥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.

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>()
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.

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>
}

附加配置

¥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.

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))}
/>
)
}

在这里,我们使用与之前相同的初始化模式,将操作分派到存储,以设置特定于路线的数据。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:

export const dynamic = 'force-dynamic'

突变后,你还应该根据需要调用 revalidatePathrevalidateTag 来使缓存无效。

¥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 a makeStore 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.