Redux 必备知识,第3部分:Redux基础数据流
- 如何在React应用中设置Redux存储(store)
- 如何通过
createSlice向Redux存储添加“切片”的reducer逻辑 - 如何使用
useSelector钩子在组件中读取Redux数据 - 如何使用
useDispatch钩子在组件中派发actions
- 熟悉Redux的核心术语和概念,如“actions”、“reducers”、“store”和“dispatch”。(相关术语解释请参见第1部分:Redux概述与概念)
- 基本了解TypeScript语法与用法
介绍
在第1部分:Redux概述与概念中,我们讨论了Redux如何通过提供一个集中管理全局应用状态的单一存储点,帮助我们构建可维护的应用。我们还讲解了核心Redux概念,如派发action对象、使用返回新状态值的reducer函数,以及通过thunks编写异步逻辑。在第2部分:Redux Toolkit应用结构中,我们看到了Redux Toolkit的configureStore和createSlice,以及React-Redux的Provider和useSelector如何配合使用,让我们在React组件中编写和交互Redux逻辑。
现在您对这些部分已有了一定了解,接下来是将知识付诸实践。我们将构建一个小型社交媒体动态APP,包含多个功能,展示一些实际用途场景,帮助您理解如何在自己的应用中使用Redux。
我们将使用TypeScript语法编写代码。您也可以用纯JavaScript使用Redux,但TypeScript能帮助避免很多常见错误,为代码提供内置文档,并让编辑器在React组件和Redux reducers中提示变量类型。我们强烈建议所有Redux项目使用TypeScript。
示例应用不代表完整的生产级项目,目的是帮助您学习Redux API和典型用法,通过有限示例指明方向。且我们在后续章节会更新初始实现,展示更优做法。请完整阅读本教程,了解所有概念的应用。
项目搭建
为本教程我们准备了一个预配置的起始项目,已经集成了React和Redux,包含了一些默认样式,并提供了假REST API接口,让我们能在应用中编写真实的API请求。您将基于此进行实际代码开发。
开始前,您可以在此打开并fork此CodeSandbox:
您也可以从此Github仓库克隆相同项目。项目配置使用Yarn 4作为包管理器,但您可以使用任意包管理器(NPM、PNPM或Bun)根据喜好。安装完依赖后,使用yarn dev命令启动本地开发服务器。
若想查看我们将构建的最终版本,可访问tutorial-steps-ts分支,或在此CodeSandbox查看最终版本。
特别感谢Tania Rascia的《在React中使用Redux》教程,启发了本页面示例。示例中也使用了她的Primitive UI CSS样式起始模板。
创建新的Redux + React项目
完成本教程后,您可能想尝试自行开发项目。我们推荐使用Vite和Next.js的Redux模板作为创建新Redux + React项目的最快方式。这些模板内置了Redux Toolkit和React-Redux,且使用了您在第1部分里见过的相同“计数器”示例,让您无需添加Redux包和设置store即可直接编写应用代码。
探索初始项目
简单介绍初始项目内容:
/public:基础CSS样式和其他静态文件如图标/srcmain.tsx:应用入口文件,渲染<App>组件。本示例中,也在页面加载时设置了假REST API。App.tsx:主应用组件,渲染顶部导航栏并处理客户端路由index.css:整个应用的样式/apiclient.ts:封装了fetch的客户端,支持HTTP GET和POST请求server.ts:模拟REST API接口,应用稍后将通过该接口获取数据
/appNavbar.tsx:渲染顶部标题栏和导航菜单
目前加载应用,您应该能看到顶部标题和欢迎信息,但没有其他功能。
准备好后,我们开始吧!
设置Redux存储
当前项目为空,需要首先进行Redux相关的单次配置。
添加Redux依赖包
打开package.json,您会看到已安装了使用Redux所需的两个包:
@reduxjs/toolkit:最新Redux包,包含构建应用用的所有Redux函数react-redux:React组件连接Redux存储所需功能
如果您是新建项目,请自行添加以上依赖包。
创建Store
第一步是创建Redux store。Redux设计原则之一是整个应用只应有一个store实例。
我们通常单独创建并导出Redux store实例。应用的文件夹结构由您决定,但一般将应用范围内的配置放在src/app/文件夹中。
这里我们在src/app/store.ts文件里创建store。
Redux Toolkit 提供了一个configureStore方法,它创建Redux store实例。该函数支持多个选项,可以更改store行为。它还自动应用了最常用的配置(包括帮您检测常见错误,以及启用Redux DevTools扩展,方便查看状态内容和操作历史)。
import { configureStore } from '@reduxjs/toolkit'
import type { Action } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
// 一个示例的slice reducer函数,演示Redux reducer的工作方式。
// 我们很快会用真实应用逻辑替换这里。
function counterReducer(state: CounterState = { value: 0 }, action: Action) {
switch (action.type) {
// 处理actions
default: {
return state
}
}
}
export const store = configureStore({
// 将根reducer作为`reducer`参数传入
reducer: {
// 声明 `state.counter` 会由 `counterReducer` 管理
counter: counterReducer
}
})
configureStore总是需要一个reducer选项,通常是包含多个“slice reducers”的对象(分别管理应用不同状态片段)。(如果需要,也可以自行创建根reducer函数,然后传给reducer参数。)
这一步,我们传入了一个模拟的counter切片reducer函数,演示设置流程。很快会用实际需要的切片替换它。
如果您用的是Next.js,配置稍复杂。具体可见Next.js下的设置文档。
提供Store给React组件
Redux本身是纯JS库,支持任何UI层。我们用的是React,所以要使React组件能访问Redux store。
实现方式是用React-Redux库的<Provider>组件包裹应用,并传入store。该组件利用React Context API将store传递给整个组件树。
切记不要直接在其他应用文件里导入Redux store! 因为应用只有一个store,直接引入可能引发循环依赖(如A导入B,B导入C,C又导入A),导致难以追踪的bug。此外,我们希望为组件和Redux逻辑编写测试,这些测试会创建各自的store实例。通过Context提供store维持灵活性,避免导入问题。
操作方式是:在入口文件main.tsx导入store,用<Provider store={store}>包裹<App>组件:
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
// 跳过mock API设置
const root = createRoot(document.getElementById('root')!)
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
查看Redux状态
有了store,我们可以用Redux DevTools扩展查看当前Redux状态。
打开浏览器开发者工具(右击页面任意处,选择“检查”),进入“Redux”标签页。这里显示了派发动作历史和当前状态值:

当前状态应为一个对象,结构类似:
{
counter: {
value: 0
}
}
该结构由传入configureStore的reducer参数定义:对象中有个叫counter的字段,counterReducer返回的状态对象形如{value}。
导出Store的类型
由于我们使用TypeScript,需要经常引用“Redux state类型”和“store中dispatch函数类型”。
这些类型需从store.ts文件导出。通过TS的typeof操作符推断store类型:
import { configureStore } from '@reduxjs/toolkit'
// 省略counter切片的代码
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
// 推断`store`的类型
export type AppStore = typeof store
// 推断`AppDispatch`类型,来自store.dispatch
export type AppDispatch = typeof store.dispatch
// 同理推断`RootState`类型
export type RootState = ReturnType<typeof store.getState>
将鼠标悬停在RootState类型处,编辑器会显示type RootState = { counter: CounterState; }。类型自动从store推断,将随reducer改动自动更新,避免重复定义且保证准确。
导出带类型的Hooks
我们要在组件中广泛使用React-Redux的useSelector和useDispatch钩子。它们需要正确引用RootState和AppDispatch类型。
为简化使用、防止重复,设定带类型的预设钩子非常有用。
React-Redux 9.1版本包含.withTypes()方法,帮我们给钩子加上类型。定义并导出这些钩子,在应用其他部分调用:
// 本文件作为重新导出带类型Redux钩子的中心
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from './store'
// 应用中统一使用它们替代原始hooks
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
至此,配置完成。开始构建应用吧!
主要帖子列表
社交媒体动态应用的核心功能是帖子的列表。我们后续会添加更多功能,先实现页面展示帖子列表。
创建帖子切片
第一步是创建Redux“切片”,存储帖子数据。
“切片”是单个功能相关的Redux reducer逻辑和actions集合,通常写在同个文件里。名字源自将根状态拆分成多个“切片”。
拿到帖子数据后,就能创建React组件在页面显示。
在src下创建新文件夹features,它下面建posts文件夹,再添加新的postsSlice.ts文件。
使用Redux Toolkit的createSlice方法创建处理帖子数据的reducer函数。reducer必须有初始状态,保证store启动时有数据。
先创建含假帖子数据的数组,用于UI开发。
导入createSlice,定义初始posts数组,将其传给createSlice,导出切片生成的reducer:
import { createSlice } from '@reduxjs/toolkit'
// 定义数据的TS类型
export interface Post {
id: string
title: string
content: string
}
// 定义类型一致的初始状态
const initialState: Post[] = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
// 创建切片并传入初始状态
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})
// 导出生成的reducer函数
export default postsSlice.reducer
每次新建切片,都要将其reducer添加进Redux store。之前创建store文件,现在修改它,导入postsReducer,移除之前的counter代码,用postsReducer替代,并把它赋予posts字段:
import { configureStore } from '@reduxjs/toolkit'
// 移除了 `counterReducer`、`CounterState`类型和`Action`导入
import postsReducer from '@/features/posts/postsSlice'
export const store = configureStore({
reducer: {
posts: postsReducer
}
})
这告诉Redux,我们希望顶层状态有个posts字段,对应所有数据由postsReducer管理。
打开Redux DevTools,确认状态如下:

显示帖子列表
既然store有了帖子数据,就创建React组件显示它们。所有和帖子feed相关代码放在posts文件夹,新建PostsList.tsx文件。(此为TypeScript加JSX语法React组件,须用.tsx后缀)
要渲染帖子列表,得先从哪儿拿数据。组件里用React-Redux库的useSelector钩子从Redux store读数据。写selector函数,参数是整个Redux state,返回组件需要的特定数据。
用TypeScript时,应使用我们在src/app/hooks.ts定义的预设钩子useAppSelector,它已有正确的RootState类型。
初始PostsList组件从store读取state.posts数组,遍历后在屏幕显示:
import { useAppSelector } from '@/app/hooks'
export const PostsList = () => {
// 从store选择`state.posts`数据到组件
const posts = useAppSelector(state => state.posts)
const renderedPosts = posts.map(post => (
<article className="post-excerpt" key={post.id}>
<h3>{post.title}</h3>
<p className="post-content">{post.content.substring(0, 100)}</p>
</article>
))
return (
<section className="posts-list">
<h2>Posts</h2>
{renderedPosts}
</section>
)
}
修改App.tsx里的路由,显示PostsList组件替代“欢迎”文字。导入PostsList,将欢迎信息替换为<PostsList />,用React Fragment包裹,因稍后会添加更多内容:
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
import { Navbar } from './components/Navbar'
import { PostsList } from './features/posts/PostsList'
function App() {
return (
<Router>
<Navbar />
<div className="App">
<Routes>
<Route
path="/"
element={
<>
<PostsList />
</>
}
></Route>
</Routes>
</div>
</Router>
)
}
export default App
完成后主页面显示帖子列表:

进步!已在Redux store中添加数据,并在React组件中渲染。
添加新帖子
浏览别人发的帖子不错,但我们也希望能自己写帖子。创建“新增帖子”表单,允许输入并保存。
先建空表单放页面。随后连接到Redux store,实现点击“保存帖子”时新增帖子。
添加新增帖子表单
在posts文件夹创建AddPostForm.tsx。包含一个文本输入框用作标题,和一个文本域输入正文:
import React from 'react'
// 表单输入字段的TS类型
// 参考:https://epicreact.dev/how-to-type-a-react-form-on-submit-handler/
interface AddPostFormFields extends HTMLFormControlsCollection {
postTitle: HTMLInputElement
postContent: HTMLTextAreaElement
}
interface AddPostFormElements extends HTMLFormElement {
readonly elements: AddPostFormFields
}
export const AddPostForm = () => {
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// 阻止表单提交跳转
e.preventDefault()
const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value
console.log('Values: ', { title, content })
e.currentTarget.reset()
}
return (
<section>
<h2>添加新帖子</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">帖子标题:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">正文:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>保存帖子</button>
</form>
</section>
)
}
注意:这里没用Redux逻辑,接下来再加。
此示例使用“非受控”输入(uncontrolled inputs),并依靠HTML5表单验证防止提交空字段,您也可自由选择表单数据的读取方式,该选择与Redux无关。
导入该组件到App.tsx,放在<PostsList />上方:
// 省略外层App定义
<Route
path="/"
element={
<>
<AddPostForm />
<PostsList />
</>
}
></Route>
页面应显示表单在标题栏下方。
保存帖子数据
接下来,修改posts切片来新增帖子项。
posts切片负责管理posts数据的所有更新。createSlice调用内有个reducers对象,目前为空。要添加postAdded函数,用于处理新增帖子。
postAdded接收两个参数:当前state与派发的action对象。posts切片仅管自身数据,因此state即帖子数组。action.payload是传入的新帖子内容。需用Redux Toolkit的PayloadAction类型声明action参数,指定action.payload的类型为Post。
更新state的操作是将新帖追加到数组,调用state.push()即可。
注意:Redux reducer必须不可变地创建新state! 在createSlice内安全调用push()、赋值等“变更语句”,因它内置了Immer库,会转成安全的不可变更新。但不要在createSlice外直接写类似变更代码!
定义postAdded后,createSlice会自动生成同名动作创建器 (action creator),导出它,方便组件派发动作。
// 导入PayloadAction类型
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
// 省略初始状态
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// 声明名为`postAdded`的case reducer
// action.payload类型为Post对象
postAdded(state, action: PayloadAction<Post>) {
// 在这里“变更”已有状态数组
// 由于createSlice内置Immer,此操作安全
state.push(action.payload)
}
}
})
// 导出自动生成的action creator
export const { postAdded } = postsSlice.actions
export default postsSlice.reducer
该函数示范了**“case reducer”**概念:slice中的reducer函数,针对单一action类型写的处理函数。等价于switch语句中的一个case:
function sliceReducer(state = initialState, action) {
switch (action.type) {
case 'posts/postAdded': {
// 更新逻辑
}
}
}
派发“新增帖子”动作
我们的AddPostForm有输入框和“保存”按钮,触发提交事件,但目前按钮无操作。需更新提交处理函数,调用postAdded动作创建器派发动作,将用户输入的帖子数据传入。
帖子也必须有唯一id。示例中的测试帖子用序号作ID。生成唯一ID方案有很多,Redux Toolkit内置nanoid函数生成随机ID,直接用它。
稍后第4部分:使用Redux数据会详细讲ID生成和派发动作。
要在组件派发动作,需访问store的dispatch函数,用React-Redux的useDispatch钩子获取。用TypeScript时,改用我们定义的带类型useAppDispatch钩子。别忘了导入postAdded动作创建器。
拿到dispatch后,表单提交时执行:
import React from 'react'
import { nanoid } from '@reduxjs/toolkit'
import { useAppDispatch } from '@/app/hooks'
import { type Post, postAdded } from './postsSlice'
// 省略表单类型定义
export const AddPostForm = () => {
// 获取store的dispatch方法
const dispatch = useAppDispatch()
const handleSubmit = (e: React.FormEvent<AddPostFormElements>) => {
// 阻止表单默认提交
e.preventDefault()
const { elements } = e.currentTarget
const title = elements.postTitle.value
const content = elements.postContent.value
// 创建新帖子对象,派发postAdded动作
const newPost: Post = {
id: nanoid(),
title,
content
}
dispatch(postAdded(newPost))
e.currentTarget.reset()
}
return (
<section>
<h2>添加新帖子</h2>
<form onSubmit={handleSubmit}>
<label htmlFor="postTitle">帖子标题:</label>
<input type="text" id="postTitle" defaultValue="" required />
<label htmlFor="postContent">正文:</label>
<textarea
id="postContent"
name="postContent"
defaultValue=""
required
/>
<button>保存帖子</button>
</form>
</section>
)
}
尝试输入标题与正文,点击“保存帖子”,您会发现帖子列表新增了新内容。
恭喜!您已构建第一个完整的React + Redux应用!
这体现了完整的Redux数据流:
- 帖子列表通过
useSelector读store初始帖子数据,渲染UI - 派发带新帖数据的
postAdded动作 - posts reducer响应动作,更新posts数组状态
- Redux store通知UI数据变化
- 帖子列表读取更新后数组,重新渲染显示新帖子
后续添加的所有功能都将沿用这个模式:添加状态切片,编写reducer,派发动作,并根据store数据渲染UI。
在Redux DevTools里查看刚刚派发的动作,点选"posts/postAdded",“Action”标签显示:

“Diff”标签也显示state.posts增加了位于索引2的新项。
记住,Redux store只应保存应用的“全局状态”! 例如,只有AddPostForm需要知道输入框里的临时值。即使用“受控”输入,也应保存在React组件状态或原生HTML输入中。用户完成后再派发Redux动作更新全局store。
您已学到
我们搭建了Redux应用的基础:store、切片和reducer,以及派发动作的UI。应用到此为止长这样:
本节回顾:
- Redux应用只有一个
store,通过<Provider>组件传递给React组件 - Redux状态通过“reducer函数”更新:
- reducer不可变地计算新状态(复制旧状态并更新副本)
- Redux Toolkit的
createSlice生成“切片reducer”,支持编写看似“变更”的代码,内部使用Immer转成不可变操作 - 这些切片reducer组合传给
configureStore的reducer字段,定义store结构和字段名
- React组件用
useSelector钩子读取store数据:- selector函数接受完整store状态,返回所需数据
- Redux store每变更,selector会重新运行。若返回数据变更,组件自动重新渲染
- React组件用
useDispatch钩子派发动作更新store:createSlice为每个reducer自动生成动作创建器- 调用
dispatch(someActionCreator())派发动作 - reducer检查动作类型,更新状态后返回新state
- 临时数据(如表单输入)应保存在React组件状态或HTML输入元素内,完成时派发Redux动作更新store
- 使用TypeScript时,应导出基于store的
RootState和AppDispatch类型,以及带类型的useSelector和useDispatch钩子
下一步?
了解了Redux数据流,继续阅读第4部分:使用Redux数据,我们将为应用添加更多功能,展示如何操作已有store数据。