跳至主要内容

Redux Essentials,第三部分:基本的 Redux 数据流

您将学到什么
  • 如何使用 createSlice 将“切片”的 reducer 逻辑添加到 Redux store 中
  • 使用 useSelector 钩子在组件中读取 Redux 数据
  • 使用 useDispatch 钩子在组件中分发 action
先决条件
  • 熟悉 Redux 的关键术语和概念,例如“action”、“reducer”、“store”和“dispatching”。(有关这些术语的解释,请参见 第 1 部分:Redux 概述和概念。)

简介

第 1 部分:Redux 概述和概念 中,我们了解了 Redux 如何通过为我们提供一个将全局应用程序状态集中放置的单一中心位置来帮助我们构建可维护的应用程序。我们还讨论了 Redux 的核心概念,例如分发 action 对象、使用返回新状态值的 reducer 函数以及使用 thunk 编写异步逻辑。在 第 2 部分:Redux Toolkit 应用程序结构 中,我们看到了 Redux Toolkit 的 configureStorecreateSlice 以及 React-Redux 的 ProvideruseSelector 等 API 如何协同工作,使我们能够编写 Redux 逻辑并从 React 组件中与该逻辑进行交互。

现在您已经对这些部分有所了解,是时候将这些知识付诸实践了。我们将构建一个小型社交媒体提要应用程序,其中将包含许多展示一些实际用例的功能。这将帮助您了解如何在自己的应用程序中使用 Redux。

注意

示例应用程序并非旨在成为完整的生产就绪项目。其目标是帮助您学习 Redux API 和典型用法模式,并通过一些有限的示例为您指明正确的方向。此外,我们构建的一些早期部分将在稍后更新,以展示更好的做法。请通读整个教程以了解所有正在使用的概念。

项目设置

在本教程中,我们创建了一个预先配置的入门项目,该项目已经设置了 React 和 Redux,包含一些默认样式,并有一个假 REST API,允许我们在应用程序中编写实际的 API 请求。您将使用它作为编写实际应用程序代码的基础。

要开始,您可以打开并分叉此 CodeSandbox

您也可以从这个 Github 仓库克隆同一个项目。克隆仓库后,您可以使用 npm install 安装项目的工具,并使用 npm start 启动它。

如果您想查看我们要构建的最终版本,可以查看tutorial-steps 分支,或查看此 CodeSandbox 中的最终版本

我们要感谢Tania Rascia,她的使用 Redux 与 React 教程帮助启发了本页面的示例。它还使用她的Primitive UI CSS 启动器 进行样式设置。

创建新的 Redux + React 项目

完成本教程后,您可能想尝试在自己的项目上工作。我们建议使用Redux 模板 for Vite 作为创建新的 Redux + React 项目的最快方法。它已经配置了 Redux Toolkit 和 React-Redux,使用您在第 1 部分中看到的相同“计数器”应用程序示例。这使您可以直接开始编写实际的应用程序代码,而无需添加 Redux 包并设置存储。

如果您想了解有关如何将 Redux 添加到项目的具体细节,请参阅此说明

详细说明:将 Redux 添加到 React 项目

Vite 的 Redux 模板已经预先配置了 Redux Toolkit 和 React-Redux。如果你要从头开始创建一个新项目,没有使用该模板,请按照以下步骤操作。

  • 添加 @reduxjs/toolkitreact-redux
  • 使用 RTK 的 configureStore API 创建一个 Redux store,并传入至少一个 reducer 函数
  • 将 Redux store 导入到应用程序的入口文件(例如 src/index.js)中
  • 使用 React-Redux 的 <Provider> 组件包装你的根 React 组件,例如
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

探索初始项目

让我们快速浏览一下初始项目包含的内容

  • /public:HTML 主页模板和其他静态文件,例如图标
  • /src
    • index.js:应用程序的入口文件。它渲染 React-Redux 的 <Provider> 组件和主要的 <App> 组件。
    • App.js:主应用程序组件。渲染顶部导航栏并处理其他内容的客户端路由。
    • index.css:整个应用程序的样式
    • /api
      • client.js:一个小型 AJAX 请求客户端,允许我们进行 GET 和 POST 请求
      • server.js:为我们的数据提供一个假的 REST API。我们的应用程序稍后将从这些假的端点获取数据。
    • /app
      • Navbar.js:渲染顶部标题和导航内容
      • store.js:创建 Redux store 实例

如果你现在加载应用程序,你应该会看到标题和欢迎消息。我们也可以打开 Redux DevTools Extension 并看到我们的初始 Redux 状态是完全空的。

有了这些,让我们开始吧!

主要帖子提要

我们社交媒体提要应用程序的主要功能将是一个帖子列表。随着我们继续进行,我们将在这个功能中添加更多内容,但首先,我们的第一个目标只是在屏幕上显示帖子条目列表。

创建帖子切片

第一步是创建一个新的 Redux "切片",它将包含我们帖子的数据。一旦我们在 Redux store 中有了这些数据,我们就可以创建 React 组件来在页面上显示这些数据。

src 中,创建一个新的 features 文件夹,在 features 中放入一个 posts 文件夹,并添加一个名为 postsSlice.js 的新文件。

我们将使用 Redux Toolkit 的 createSlice 函数来创建一个 reducer 函数,该函数知道如何处理我们的帖子数据。reducer 函数需要包含一些初始数据,以便 Redux store 在应用程序启动时加载这些值。

现在,我们将创建一个包含一些假的帖子对象的数组,以便我们可以开始添加 UI。

我们将导入createSlice,定义我们的初始帖子数组,将其传递给createSlice,并导出createSlice为我们生成的帖子 reducer 函数

features/posts/postsSlice.js
import { createSlice } from '@reduxjs/toolkit'

const initialState = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]

const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})

export default postsSlice.reducer

每次创建新的切片时,都需要将它的 reducer 函数添加到 Redux 存储中。我们已经创建了一个 Redux 存储,但目前它没有任何数据。打开app/store.js,导入postsReducer函数,并更新对configureStore的调用,以便将postsReducer作为名为posts的 reducer 字段传递

app/store.js
import { configureStore } from '@reduxjs/toolkit'

import postsReducer from '../features/posts/postsSlice'

export default configureStore({
reducer: {
posts: postsReducer
}
})

这告诉 Redux 我们希望我们的顶级状态对象内有一个名为posts的字段,并且所有state.posts的数据将在分派操作时由postsReducer函数更新。

我们可以通过打开 Redux DevTools 扩展并查看当前状态内容来确认这一点

Initial posts state

显示帖子列表

现在我们已经在存储中有一些帖子数据,我们可以创建一个显示帖子列表的 React 组件。所有与我们的 feed 帖子功能相关的代码都应该放在posts文件夹中,因此请在其中创建一个名为PostsList.js的新文件。

如果要渲染帖子列表,我们需要从某个地方获取数据。React 组件可以使用 React-Redux 库中的useSelector钩子从 Redux 存储中读取数据。您编写的“选择器函数”将使用整个 Reduxstate对象作为参数调用,并且应该返回该组件从存储中需要的特定数据。

我们最初的PostsList组件将从 Redux 存储中读取state.posts值,然后遍历帖子数组并在屏幕上显示每个帖子

features/posts/PostsList.js
import React from 'react'
import { useSelector } from 'react-redux'

export const PostsList = () => {
const posts = useSelector(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.js中的路由,以便显示PostsList组件而不是“欢迎”消息。将PostsList组件导入App.js,并将欢迎文本替换为<PostsList />。我们还将它包装在一个React 片段中,因为我们很快将在主页面中添加其他内容

App.js
import React from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from 'react-router-dom'

import { Navbar } from './app/Navbar'

import { PostsList } from './features/posts/PostsList'

function App() {
return (
<Router>
<Navbar />
<div className="App">
<Switch>
<Route
exact
path="/"
render={() => (
<React.Fragment>
<PostsList />
</React.Fragment>
)}
/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}

export default App

添加完后,我们应用程序的主页应该看起来像这样

Initial posts list

进度!我们已将一些数据添加到 Redux 存储中,并在 React 组件中将其显示在屏幕上。

添加新帖子

查看人们写的帖子很好,但我们希望能够写自己的帖子。让我们创建一个“添加新帖子”表单,让我们可以编写帖子并保存它们。

我们将首先创建空表单并将其添加到页面。然后,我们将表单连接到我们的 Redux 存储,以便当我们点击“保存帖子”按钮时添加新帖子。

添加新帖子表单

在我们的 posts 文件夹中创建 AddPostForm.js。我们将添加一个用于帖子标题的文本输入和一个用于帖子正文的文本区域

features/posts/AddPostForm.js
import React, { useState } from 'react'

export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')

const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)

return (
<section>
<h2>Add a New Post</h2>
<form>
<label htmlFor="postTitle">Post Title:</label>
<input
type="text"
id="postTitle"
name="postTitle"
value={title}
onChange={onTitleChanged}
/>
<label htmlFor="postContent">Content:</label>
<textarea
id="postContent"
name="postContent"
value={content}
onChange={onContentChanged}
/>
<button type="button">Save Post</button>
</form>
</section>
)
}

将该组件导入 App.js,并将其添加到 <PostsList /> 组件的正上方

App.js
<Route
exact
path="/"
render={() => (
<React.Fragment>
<AddPostForm />
<PostsList />
</React.Fragment>
)}
/>

您应该看到表单显示在页面标题下方。

保存帖子条目

现在,让我们更新我们的帖子切片以将新的帖子条目添加到 Redux 存储中。

我们的帖子切片负责处理对帖子数据的所有更新。在 createSlice 调用中,有一个名为 reducers 的对象。现在,它为空。我们需要在其中添加一个 reducer 函数来处理添加帖子的情况。

reducers 中,添加一个名为 postAdded 的函数,它将接收两个参数:当前的 state 值和已分派的 action 对象。由于帖子切片了解它负责的数据,因此 state 参数将是帖子数组本身,而不是整个 Redux 状态对象。

action 对象将具有我们的新帖子条目作为 action.payload 字段,我们将把该新帖子对象放入 state 数组中。

当我们编写 postAdded reducer 函数时,createSlice 将自动生成一个具有相同名称的“动作创建者”函数。我们可以导出该动作创建者并在我们的 UI 组件中使用它来在用户点击“保存帖子”时分派动作。

features/posts/postsSlice.js
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded(state, action) {
state.push(action.payload)
}
}
})

export const { postAdded } = postsSlice.actions

export default postsSlice.reducer
危险

记住:**reducer 函数必须始终以不可变的方式创建新的状态值,通过创建副本!**在 `createSlice()` 内部调用像 `Array.push()` 这样的变异函数或修改像 `state.someField = someValue` 这样的对象字段是安全的,因为它使用 Immer 库将这些变异内部转换为安全的不可变更新,但是**不要尝试在 `createSlice` 之外变异任何数据!**

分发“帖子已添加”操作

我们的 `AddPostForm` 有文本输入和一个“保存帖子”按钮,但按钮目前还没有任何作用。我们需要添加一个点击处理程序,它将分发 `postAdded` 操作创建者并传入一个包含用户编写的标题和内容的新帖子对象。

我们的帖子对象还需要一个 `id` 字段。现在,我们最初的测试帖子使用了一些虚假的数字作为它们的 ID。我们可以编写一些代码来确定下一个递增的 ID 编号应该是什么,但如果我们生成一个随机的唯一 ID 会更好。Redux Toolkit 有一个 `nanoid` 函数,我们可以用来实现这一点。

信息

我们将在第 4 部分:使用 Redux 数据中详细讨论生成 ID 和分发操作。

为了从组件中分发操作,我们需要访问存储的 `dispatch` 函数。我们可以通过调用 React-Redux 中的 `useDispatch` 钩子来获取它。我们还需要将 `postAdded` 操作创建者导入到此文件中。

一旦我们在组件中拥有了 `dispatch` 函数,我们就可以在点击处理程序中调用 `dispatch(postAdded())`。我们可以从 React 组件的 `useState` 钩子中获取标题和内容值,生成一个新的 ID,并将它们组合成一个新的帖子对象,然后传递给 `postAdded()`。

features/posts/AddPostForm
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'

import { postAdded } from './postsSlice'

export const AddPostForm = () => {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')

const dispatch = useDispatch()

const onTitleChanged = e => setTitle(e.target.value)
const onContentChanged = e => setContent(e.target.value)

const onSavePostClicked = () => {
if (title && content) {
dispatch(
postAdded({
id: nanoid(),
title,
content
})
)

setTitle('')
setContent('')
}
}

return (
<section>
<h2>Add a New Post</h2>
<form>
{/* omit form inputs */}
<button type="button" onClick={onSavePostClicked}>
Save Post
</button>
</form>
</section>
)
}

现在,尝试输入标题和一些文本,然后点击“保存帖子”。您应该在帖子列表中看到该帖子的新条目。

恭喜!您刚刚构建了第一个可运行的 React + Redux 应用程序!

这展示了完整的 Redux 数据流循环

  • 我们的帖子列表使用 useSelector 从商店读取初始帖子集,并渲染了初始 UI
  • 我们分发了包含新帖子条目的数据的 postAdded 操作
  • 帖子 reducer 看到 postAdded 操作,并使用新条目更新了帖子数组
  • Redux 商店告诉 UI 一些数据已更改
  • 帖子列表读取更新后的帖子数组,并重新渲染自身以显示新帖子

我们将在之后添加的所有新功能都将遵循您在此处看到的相同基本模式:添加状态切片,编写 reducer 函数,分发操作,以及根据 Redux 商店中的数据渲染 UI。

我们可以检查 Redux DevTools 扩展以查看我们分发的操作,并查看 Redux 状态如何响应该操作而更新。如果我们在操作列表中点击 "posts/postAdded" 条目,则“操作”选项卡应如下所示

postAdded action contents

“差异”选项卡还应显示 state.posts 添加了一个新项目,该项目位于索引 2 处。

请注意,我们的 AddPostForm 组件内部有一些 React useState 钩子,用于跟踪用户输入的标题和内容值。请记住,**Redux 商店应该只包含被认为是应用程序“全局”的数据!** 在这种情况下,只有 AddPostForm 需要知道输入字段的最新值,因此我们希望将该数据保存在 React 组件状态中,而不是尝试将临时数据保存在 Redux 商店中。当用户完成表单后,我们分发一个 Redux 操作,以根据用户输入使用最终值更新商店。

您学到了什么

让我们回顾一下您在本节中学到的内容

总结
  • Redux 状态通过“reducer 函数”更新:
    • Reducer 始终通过复制现有状态值并使用新数据修改副本,以不可变的方式计算新状态
    • Redux Toolkit createSlice 函数为您生成“切片 reducer”函数,并允许您编写“可变”代码,该代码将转换为安全的不可变更新
    • 这些切片 reducer 函数被添加到 configureStore 中的 reducer 字段,这定义了 Redux 商店内部的数据和状态字段名称
  • React 组件使用 useSelector 钩子从 store 中读取数据
    • 选择器函数接收整个 state 对象,并应返回一个值
    • 每当 Redux store 更新时,选择器都会重新运行,如果它们返回的数据发生了变化,组件将重新渲染
  • React 组件使用 useDispatch 钩子调度操作来更新 store
    • createSlice 将为我们添加到 slice 中的每个 reducer 生成操作创建器函数
    • 在组件中调用 dispatch(someActionCreator()) 来调度操作
    • reducer 将运行,检查此操作是否相关,并在适当的情况下返回新的状态
    • 像表单输入值这样的临时数据应该保留为 React 组件状态。当用户完成表单时,调度一个 Redux 操作来更新 store。

以下是应用程序目前的样子

下一步?

现在您已经了解了基本的 Redux 数据流,请继续 第 4 部分:使用 Redux 数据,我们将为我们的应用程序添加一些额外的功能,并查看如何使用 store 中已有的数据的示例。