第 12 章 Redux 和 Hook

Flying
2021-05-26 / 0 评论 / 118 阅读 / 正在检测是否收录...

在上一章中,我们了解了 React 类组件,以及如何从现有的基于类组件的项目迁移到基于 Hook 的项目。然后,我们了解了这两种解决方案之间的权衡,并讨论了何时以及如何迁移现有项目。

在本章中,我们将把我们在上一章中创建的 ToDo 应用程序转换为 Redux 应用程序。首先,我们将学习什么是 Redux,包括 Redux 的三个原则。我们还将了解何时在应用程序中使用 Redux 是有意义的,并且它并不适合每个应用程序。此外,我们将学习如何使用 Redux 处理状态。之后,我们将学习如何将 Redux 与 Hook 一起使用,以及如何将现有的 Redux 应用程序迁移到 Hook。最后,我们将学习 Redux 的权衡,以便能够决定哪种解决方案最适合某个用例。在本章结束时,您将完全了解如何使用 Hook 编写 Redux 应用程序。

本章将介绍以下主题:

  • 什么是 Redux、何时使用它以及为什么应该使用它
  • 使用 Redux 处理状态
  • 将 Redux 与 Hook 一起使用
  • 迁移 Redux 应用程序
  • 学习 Redux 的权衡取舍

技术要求

应该已经安装了相当新版本的 Node.js(vl2.0 或更高版本)。还需要安装 Node.js 的 npm 包管理器。

本章的代码可以在 GitHub 存储库中找到:https://github.com/PacktPublishing/Learn-React-Hooks/tree/master/Chapter12

观看以下视频,了解代码的实际应用:

http://bit.ly/2Mm9yoC

请注意,强烈建议您自己编写代码。不要简单地运行已提供的代码示例。重要的是自己编写代码,以便您能够正确学习和理解。但是,如果遇到任何问题,始终可以参考代码示例。

现在,让我们从本章开始。

什么是 Redux?

正如我们之前所了解的,应用程序中有两种状态:

  • 本地状态:例如,处理输入字段数据
  • 全局状态:例如,存储当前登录的用户

在本书的前面,我们使用状态 Hook 处理局部状态,并使用 reducer Hook 处理更复杂的状态(通常是全局状态)。

Redux 是一种解决方案,可用于处理 React 应用程序中的各种状态。它提供单个状态树对象,其中包含所有应用程序状态。这类似于我们在博客应用程序中对 Reducer Hook 所做的。传统上,Redux 也经常用于存储本地状态,这使得状态树非常复杂。

Redux 基本上 Store 由五个元素组成:

  • Store:包含状态,这是一个描述应用程序完整状态的对象——{ todos: [], filter: 'all' }
  • Action:描述状态修改的对象——{ type: 'FILTER_TODOS', filter: 'completed' }
  • Action creator:创建 action 对象的函数——(filter) => ({type: 'FILTER_TODOS', filter })
  • Reducer:获取当前 state 值和 action 对象 并返回新状态的函数 ——(state, action) => { ... }
  • Connector:通过将 Redux 状态和 action creator 作为 props 注入将现有组件连接到 Redux 的高阶组件—— connect(mapStateToProps, mapDispatchToProps)(Component)

在 Redux 生命周期中,Store 包含定义 UI 的状态。UI 通过 Connector 连接到 Redux store。然后,用户与 UI 的交互会触发 action,该 action 将发送到 Reducer。然后,Reducer 更新 Store 中的状态。

我们可以在下图中看到 Redux 生命周期的可视化:

ch11-redux-lifecycle.png
Redux 生命周期的可视化

如您所见,我们已经了解了其中三个组件:store(状态树)、action 和 reducer。Redux 就像是 Reducer Hook 的更高级版本。不同之处在于,使用 Redux,我们总是将状态调度到单个 reducer,从而更改单个状态。Redux 不应有多个实例。通过这个限制,我们可以确定我们的整个应用程序状态包含在单个对象中,这允许我们仅从 Redux store 中重建整个应用程序状态。

由于只有一个包含所有状态的存储,我们可以通过将 Redux store 保存在崩溃报告中来轻松调试错误状态,或者我们可以在调试期间自动重播某些 action,这样我们就不需要一遍又一遍手动输入文本并单击按钮。

此外,Redux 提供了中间件,可以简化我们处理异步请求的方式,例如从服务器获取数据。现在我们了解了 Redux 是什么,在下一节中,我们将学习 Redux 的三个基本原则。

Redux 的三个原则

Redux API 非常小,实际上只包含少数几个函数。Redux 之所以如此强大,是因为在使用库时应用程序于代码的一组特定规则。这些规则允许编写易于扩展、测试和调试的可缩放应用程序。

Redux 基于三个基本原则:

  • 单一事实来源
  • 只读状态
  • 状态更改由纯函数处理

单一事实来源

此 Redux 原则指出,数据应始终具有单一的事实来源。这意味着全局数据来自单个 Redux store,而本地数据来自于其它,比如某个 State Hook。每种数据只有一个来源。因此,应用程序变得更易于调试,并且不易出错。

只读状态

使用 Redux,无法直接修改应用程序状态。只能通过调度 action 来更改状态。此原则使状态更改可预测:如果触发任何 action,则应用程序状态不会更改。此外,一次处理一个 action,因此我们不必处理竞态条件。最后,action 是纯 JavaScript 对象,这使得它们易于序列化、记录、存储或重放。因此,调试和测试 Redux 应用程序变得非常容易。

状态更改由纯函数处理

纯函数是给定相同输入的函数,将始终返回相同的输出。Redux 中的 reducer 是纯函数,因此,给定相同的状态和 action,它们将始终返回相同的新状态。

例如,以下 reducer 是一个非纯函数,因为使用相同的输入多次调用该函数会导致不同的输出:

let i = 0
function counterReducer(state, action) {
  if (a) {
    i++
  }
  return i
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // 打印 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // 打印 2

为了将这个 reducer 变成一个纯函数,我们必须确保它不依赖于外部状态,并且只使用它的参数进行计算:

function counterReducer(state, action) {
  if (action.type === 'INCREMENT') {
    return state + 1
  }
  return state
}

console.log(counterReducer(0, { type: 'INCREMENT' })) // 打印 1
console.log(counterReducer(0, { type: 'INCREMENT' })) // 打印 1

对 reducer 使用纯函数可以预测,并且易于测试和调试。使用 Redux,我们需要小心始终返回新状态,而不是修改现有状态。因此,例如,我们不能在数组状态上使用 Array.push(),因为它会修改现有数组;我们需要使用 Array.concat() 来创建一个新数组。对象也是如此,我们需要使用 rest/扩展运算符语法来创建新对象,而不是修改现有对象。例如 { ...state, completed: true }

现在我们已经了解了 Redux 的三个基本原则,我们可以继续在实践中使用 Redux,在我们的 ToDo 应用程序中使用 Redux 实现状态处理。

使用 Redux 处理状态

Redux 的状态管理实际上与使用 Reducer Hook 非常相似。我们首先定义 State 对象,然后是 action,最后是我们的 reducer。Redux 中的另一种模式是创建返回 action 对象的函数,即所谓的 action creator。此外,我们需要用 Provider 组件包装整个应用程序,并将组件连接到 Redux store,以便能够使用 Redux 状态和 action creator。

安装 Redux

首先,我们需要安装 Redux、React Redux 和 Redux Thunk。让我们看看每个人分别做了什么:

  • Redux 本身只处理 JavaScript 对象,因此它提供存储,处理 action 和 action creator,并处理 reducer。
  • React Redux 提供了连接器,以便将 Redux 连接到我们的 React 组件。
  • Redux Thunk 是一个中间件,允许我们处理 Redux 中的异步请求。

Redux 与 React 结合使用将全局状态管理卸载到 Redux,而 React 处理渲染应用程序和本地状态:

ch11-react-redux.jpg
React 和 Redux 如何协同工作的插图

要安装 Redux 和 React Redux,我们将使用 npm。执行以下命令:

npm install --save redux react-redux redux-thunk

现在所有必需的库都已安装,我们可以开始设置我们的 Redux store 了。

定义 state、action 和 reducer

开发 Redux 应用程序的第一步是定义 state,然后是将要更改 state 的 action,最后是执行 state 更改的 reducer 函数。在我们的 ToDo 应用程序中,我们已经定义了 state action 和 reducer,以便使用 Reducer Hook。在这里,我们简单地回顾一下我们在上一章中定义的内容。

State

我们的 ToDo 应用程序的完整 State 对象有两个键:待办事项数组和一个用于指定当前选定的 filter 值的字符串。初始状态如下所示:

{
  "todos": [
    { "id": 1, "title": "Write React Hooks book", "completed": true },
    { "id": 2, "title": "Promote book", "completed": false }
  ],
  "filter": "all"
}

正如我们所看到的,在 Redux 中,State 对象包含对我们的应用程序很重要的所有状态。在这种情况下,应用程序状态由 todos 数组和 filter 组成。

Action

我们的应用程序接受以下五个 action:

  • FETCH_TODOS: 获取新的待办事项列表——{ type: 'FETCH_TODOS', todos: [] }
  • ADD_TODO: 插入新的待办事项——{ type: 'ADD_TODO', title: 'Test ToDo app' }
  • TOGGLE_TODO:切换待办事项的 completed 值—— { type: 'TOGGLE_TODO', id: 'xxx' }
  • REMOVE_TODO: 删除待办事项——{ type: 'REMOVE_TODO', id: 'xxx' }
  • FILTER_TODOS: 过滤待办事项——{ type: 'FILTER_TODOS', filter: 'completed' }

Reducer

我们定义了三个 reducer——两个用于状态的相应部分,一个用来组合其他两个 reducer 的 app reducer 。筛选器 reducer 等待 FILTER_TODOS action,然后相应地设置新筛选器。todos reducer 监听其他与 todos 相关的 action,并通过添加、删除或修改元素来调整 todos 数组。然后,app reducer 将两者合并,并将 action 传递给它们。在定义了创建 Redux 应用程序所需的所有元素之后,我们现在可以设置 Redux store。

设置 Redux store

为了让事情一开始就简单,并展示 Redux 的工作原理,我们现在不打算使用连接器。我们只需要用 Redux 替换之前由 Reducer Hook 提供的 state 对象和dispatch 函数。

现在让我们设置 Redux store:

  1. 编辑 src/App.js,并从 Redux 库中导入 useState Hook 和 createStore 函数:
import React, { usestate, useEffect, useMemo } from 'react'
import { createstore } from 'redux'
  1. 在导入语句下方和 App 函数定义之前,我们将初始化 Redux store。我们首先定义初始状态:
const initialState = { todos: [], filter: 'all' }
  1. 接下来,我们将使用 createStore 函数来定义 Redux store,方法是使用现有的 appReducer 函数并传递 initialState 对象:
const store = createStore(appReducer, initialState)
请注意,在 Redux 中,通过将状态传递给 createStore 来初始化状态并不是最佳实践。但是,对于 Reducer Hook,我们需要这样做。在 Redux 中,我们通常通过在 reducer 函数中设置默认值来初始化状态。我们将在本章后面学习更多关于通过 Redux reducer 初始化状态的信息。
  1. 现在,我们可以从 store 中获取 dispatch 函数:
const { dispatch } = store
  1. 下一步是在 App 函数中删除以下 reducer Hook 定义:
const [state, dispatch] = useReducer(appReducer, { todos: [], filter: 'all' })

它被一个简单 State Hook 所取代,将存储我们的 Redux 状态:

const [state, setState] = useState(initialState)
  1. 最后,我们定义一个 Effect Hook,以使 State Hook 与 Redux store 状态保持同步:
useEffect(() => {
  const unsubscribe = store.subscribe(() => setState(store.getState()))
  return unsubscribe
}, [])

如我们所见,该应用程序仍然以与以前完全相同的方式运行。Redux 的工作方式与 Reducer Hook 非常相似,但具有更多功能。

但是,在如何定义 action 和 reducer 方面存在细微差异,我们将在以下部分中了解这一点。

示例代码

示例代码可以在 Chapter12/chapter12_1 文件夹中找到。

只需运行 npm install 以安装所有依赖项,运行 npm start 即可启动应用程序,然后访问 [http://localhost:3000(如果它没有自动打开)。

定义 action 类型

创建完整的 Redux 应用程序的第一步是定义所谓的 action 类型。它们将用于在 action creator 中创建 action,并在 reducer 中处理 action。这里的想法是避免在定义或比较 action 的 type 属性时输入错误。

现在让我们定义 action 类型:

  1. 创建一个新的 src/actionTypes.js 文件。
  2. 在新创建的文件中定义并导出以下常量:
export const FETCH_TODOS = 'FETCH_TODOS'
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO' export const REMOVE_TODO = 'REMOVE_TODO'
export const FILTER_TODOS = 'FILTER_TODOS'

现在我们已经定义了 action 类型,我们可以开始在 action creator 和 reducer 中使用它们。

定义 action creator

定义 action 类型后,我们需要定义 action 本身。在此过程中,我们将定义将返回 action 对象的函数。这些函数称为 action creator ,它们有两种类型:

  • 同步 action creator :这些 creator 仅返回一个 action 对象
  • 异步 action creator:这些 creator 返回 async 函数,该函数稍后将调度一个 action

我们将从定义同步 action creator 开始,然后我们将学习如何定义异步 action creator。

定义同步 action creator

我们之前已经在 src/App.js 中定义了 action creator 函数。现在,我们可以从 App 组件中复制它们,确保调整 type 属性以使用 action 类型常量,而不是静态字符串。

现在让我们定义同步 action creator:

  1. 创建一个新的 src/actions.js 文件。
  2. 导入所有 action 类型,我们将需要这些类型来创建 action:
import { ADD_TODO, TOGGLE_TODO, REMOVE_TODO, FILTER_TODOS } from './actionTypes'
  1. 现在,我们可以定义和导出我们的 action creator 函数:
export function addTodo(title) {
  return { type: ADD_TODO, title }
}


export function toggleTodo(id) {
  return { type: TOGGLE_TODO, id }
}

export function removeTodo(id) {
  return { type: REMOVE_TODO, id }
}

export function filterTodos(filter) {
  return { type: FILTER_TODOS, filter }
}

如我们所见,同步 action creator 只需创建和返回 action 对象。

定义异步 action creator

下一步是为 fetchTodos 定义异步 action creator。在这里,我们将使用 async/await 结构。

我们现在将使用 async 函数来定义 fetchTodos action creator:

  1. src/actions.js 中,首先导入 FETCH_TODOS action 类型和 fetchAPITodos 函数:
import {
  FETCH_TODOs,
  ADD_TODO,
  TOGGLE_TODO,
  REMOVE_TODO,
  FILTER_TODOS,
} from './actionTypes'

import { fetchAPITodos } from './api'
  1. 然后,定义一个新的 action creator 函数,该函数将返回一个 async 函数,该函数将 dispatch 函数作为参数:
export function fetchTodos () {
  return async (dispatch) => {
  1. 在这个 async 函数中,我们现在将调用 API 函数,dispatch 我们的 action:
const todos = await fetchAPITodos()
    dispatch({ type: FETCH_TODOS, todos })
  }
}

如我们所见,异步 action creator 返回一个函数,该函数将在稍后调度 action。

调整 store

为了能够在 Redux 中使用异步 action creator 函数,我们需要安装 redux-thunk 中间件。此中间件检查 action creator 是否返回了函数,而不是普通对象,如果是这种情况,它将执行该函数,同时将 dispatch 函数作为参数传递给它。

现在,让我们调整 store 以允许异步 action creator:

  1. 创建一个新的 src/configureStore.js 文件。
  2. 从 Redux 导入 createStoreapplyMiddleware 函数:
import { createStore, applyMiddleware } from 'redux'
  1. 接下来,导入 thunk 中间件和 appReducer 函数:
import thunk from 'redux-thunk' import appReducer from './reducers'
  1. 现在,我们可以定义 store 并对其应用 thunk 中间件:
const store = createStore(appReducer, applyMiddleware(thunk))
  1. 最后,我们导出 store
export default store

使用 redux-thunk 中间件,我们现在可以调度稍后将调度 action 的函数,这意味着我们的异步 action creator 现在可以正常工作。

调整 reducer

如前文所述,Redux reducer 与 Dispatch Hook 的不同之处在于它们具有某些约定:

  • 每个 reducer 都需要通过在函数声明中定义默认值来设置其初始状态
  • 每个 reducer 都需要返回未处理的 action 的当前状态

我们现在将调整现有的 reducer,以便它们遵循这些约定。第二个约定已经实现,因为我们之前定义了单个应用程序 reducer,以避免具有多个调度函数。

在 Redux reducer 中设置初始状态

因此,我们将专注于第一个约定——通过在函数参数中定义默认值来设置初始状态,如下所示:

  1. 编辑 src/reducers.js 并从 Redux 导入 combineReducers 函数:
import { combineReducers } from 'redux'
  1. 然后,将 filterReducer 重命名为 filter,并设置默认值:
function filter (state = 'all', action) {
  1. 接下来,编辑 todosReducer 并重复相同的过程:
function todos (state = [], action) {
  1. 最后,我们将使用 combineReducers 函数来创建 appReducer 函数。我们现在可以执行以下操作,而不是手动创建函数:
const appReducer = combineReducers({ todos, filter }) 
export default appReducer

正如我们所看到的,Redux reducer 与 Dispatch Hook 非常相似。Redux 甚至提供了一个函数,允许我们将多个 reducer 函数组合到一个 app reducer 中!

连接组件

现在,是时候引入连接器和容器组件了。在 Redux 中,我们可以使用 connect 的高阶组件将现有组件连接到 Redux,方法是将 state 和 action creator 作为 props 注入其中。

Redux 定义了两种不同类型的组件:

  • 表示组件:React 组件,正如我们迄今为止一直在定义它们的那样
  • 容器组件:将表示组件连接到 Redux 的 React 组件

容器组件使用连接器将 Redux 连接到表示组件。此连接器接受两个函数:

  • mapStateToProps(state):获取当前的 Redux 状态,并返回要传递给组件的 props 对象;用于将状态传递给组件
  • mapDispatchToProps(dispatch):从 Redux store 中获取 dispatch 函数,并返回要传递给组件的 props 对象;用于将 action creator 传递给组件

我们现在将为现有的表示组件定义容器组件:

  1. 首先,我们为所有表示组件创建一个新的 src/components/ 文件夹。
  2. 然后,我们将所有现有的组件文件复制到 src/components/ 文件夹中,并调整以下文件的导入语句:

AddTodo.jsApp.jsHeader.jsTodoFilter.jsTodoItem.jsTodoList.js

连接 AddTodo 组件

我们现在将开始将组件连接到 Redux store。表示组件可以保持与以前相同。我们只创建新的组件(容器组件)来包装表示组件,并将某些 props 传递给它们。

现在让我们连接 AddTodo 组件:

  1. 为我们所有的容器组件创建一个新的 src/containers/ 文件夹。
  2. 创建一个新的 src/containers/ConnectedAddTodo.js 文件。
  3. 在此文件中,我们从 react-redux 导入 connect 函数,从 redux 导入 bindActionCreators 函数:
import { connect } from 'react-redux' 
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入 addTodo action creator 和 AddTodo 组件:
import { addTodo } from '../actions'
import AddTodo from '../components/AddTodo'
  1. 现在,我们将定义 mapStateToProps 函数。由于这个组件不处理来自 Redux 的任何状态,我们可以在这里简单地返回一个空对象:
function mapStateToProps(state) {
  return {}
}
  1. 然后,我们定义 mapDispatchToProps 函数。这里我们使用 bindActionCreators 来包装 action creator 和 dispatch 函数:
function mapDispatchToProps(dispatch) {
  return bindActionCreators({ addTodo }, dispatch)
}

此代码本质上与手动包装 action creator 相同,如下所示:

function mapDispatchToProps(dispatch) {
  return {
    addTodo: (...args) => dispatch(addTodo(...args)),
  }
}
  1. 最后,我们使用 connect 函数将 AddTodo 组件连接到 Redux:
export default connect(mapStateToProps, mapDispatchToProps)(AddTodo)

现在,我们的 AddTodo 组件已成功连接到 Redux store。

连接 TodoItem 组件

接下来,我们将连接 TodoItem 组件,以便我们可以在下一步的 TodoList 组件中使用它。

现在让我们连接 TodoItem 组件:

  1. 创建一个新的 src/containers/ConnectedTodoItem.js 文件。
  2. 在此文件中,我们从 react-redux 导入 connect 函数,从 redux 导入 bindActionCreators 函数:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入 toggleTodoremoveTodo action creator 以及 TodoItem 组件:
import { toggleTodo, removeTodo } from '../actions'
import TodoItem from '../components/TodoItem'
  1. 同样,我们只从 mapStateToProps 返回一个空对象:
function mapStateToProps(state) {
  return {}
}
  1. 这一次,我们将两个 action creator 绑定到 dispatch 函数:
function mapDispatchToProps(dispatch) {
  return bindActionCreators({ toggleTodo, removeTodo }, dispatch)
}
  1. 最后,我们连接组件,并将其导出:
export default connect(mapStateToProps, mapDispatchToProps)(TodoItem)

现在,我们的 TodoItem 组件已成功连接到 Redux store。

连接 TodoList 组件

连接 TodoItem 组件后,我们现在可以在 TodoList 组件中使用 ConnectedTodoItem 组件。现在让我们连接 TodoList 组件:

  1. 编辑 src/components/TodoList.js,并调整导入语句,如下所示:
import ConnectedTodoItem from '../containers/ConnectedTodoItem'
  1. 然后,将从函数返回的组件重命名为 ConnectedTodoItem
import filteredTodos.map((item) => (
  <ConnectedTodoItem {...item} key={item.id} />
))
  1. 现在,创建一个新的 src/containers/ConnectedTodoList.js 文件。
  2. 在此文件中,我们仅从 react-redux 导入 connect 函数,因为这次我们不打算绑定 action creator:
import { connect } from 'react-redux'
  1. 接下来,我们导入 TodoList 组件:
import TodoList from '../components/TodoList'
  1. 现在,我们定义 mapStateToProps 函数。这一次,我们使用解构从 state 对象中获取 todosfilter,并返回它们:
function mapStateToProps(state) {
  const { filter, todos } = state
  return { filter, todos }
}
  1. 接下来,我们定义 mapDispatchToProps 函数,其中我们只返回一个空对象,因为我们不会将任何 action creator 传递给 TodoList 组件:
function mapDispatchToProps(dispatch) {
  return {}
}
  1. 最后,我们连接并导出连接的 TodoList 组件:
export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

现在,我们的 TodoList 组件已成功连接到 Redux store。

调整 TodoList 组件

现在我们已经连接了 TodoList 组件,我们可以将筛选逻辑从 App 组件移动到 TodoList 组件,如下所示:

  1. src/components/TodoList.js 中导入 useMemo Hook:
import React, { useMemo } from 'react'
  1. 编辑 src/components/App.js,并删除以下代码:
const filteredTodos = useMemo(() => {
  const { filter, todos } = state
  switch (filter) {
    case 'active':
      return todos.filter((t) => t.completed === false)
    case 'completed':
      return todos.filter((t) => t.completed === true)
    default:
    case 'all':
      return todos
  }
}, [state])
  1. 现在,编辑 src/components/TodoList.js,并在此处添加 filteredTodos 代码。请注意,我们从 state 对象中删除了解构,因为组件已经接收了 filtertodos 值作为 props。我们还相应地调整了依赖数组:
const filteredTodos = useMemo(() => {
  switch (filter) {
    case 'active':
      return todos.filter((t) => t.completed === false)

    case 'completed':
      return todos.filter((t) => t.completed === true)

    default:
    case 'all':
      return todos
  }
}, [filter, todos])

现在,我们的过滤逻辑位于 TodoList 组件中,而不是 App 组件中。让我们继续连接其它组件。

连接 TodoFilter 组件

接下来是 TodoFilter 组件。在这里,我们将同时使用 mapStateToPropsmapDispatchToProps

现在让我们连接 TodoFilter 组件:

  1. 创建一个新的 src/containers/ConnectedTodoFilter.js 文件。
  2. 在此文件中,我们从 react-redux 导入 connect 函数,从 redux 导入 bindActionCreators 函数:
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入 filterTodos action creator 和 TodoFilter 组件:
import { filterTodos } from '../actions'
import TodoFilter from '../components/TodoFilter'
  1. 我们使用解构从 state 对象中获取 filter,然后返回它:
function mapStateToProps(state) {
  const { filter } = state
  return { filter }
}
  1. 接下来,我们绑定并返回 filterTodos action creator:
function mapDispatchToProps(dispatch) {
  return bindActionCreators({ filterTodos }, dispatch)
}
  1. 最后,我们连接组件并将其导出:
export default connect(mapStateToProps, mapDispatchToProps)(TodoFilter)

现在,我们的 TodoFilter 组件已成功连接到 Redux store。

连接 App 组件

现在唯一仍需要连接的组件是 App 组件。在这里,我们将注入 fetchTodos action creator ,并更新组件,使其可以使用所有其他组件的连接版本。

现在让我们连接 App 组件:

  1. 编辑 src/components/App.js,并调整以下导入语句:
import ConnectedAddTodo from '../containers/ConnectedAddTodo'
import ConnectedTodoList from '../containers/ConnectedTodoList'
import ConnectedTodoFilter from '../containers/ConnectedTodoFilter'
  1. 此外,调整从函数返回的以下组件:
return (
  <div style={{ width: 400 }}>
    <Header />
    <ConnectedAddTodo />
    <hr />
    <ConnectedTodoList />
    <hr />
    <ConnectedTodoFilter />
  </div>
)
  1. 现在,我们可以创建连接组件。创建一个新的 src/containers/ConnectedApp.js 文件。
  2. 在这个新创建的文件中,我们从 react-redux 导入 connect 函数,从 redux 导入 bindActionCreators 函数:
import { connect } from 'react-redux' 
import { bindActionCreators } from 'redux'
  1. 接下来,我们导入 fetchTodos action creator 和 App 组件:
import { fetchTodos } from '../actions'
import App from '../components/App'
  1. 我们已经在其他组件中处理了状态的各个部分,因此无需将任何状态注入到 App 组件中:
function mapStateToProps(state) {
  return {}
}
  1. 然后,我们绑定并返回 fetchTodos action creator:
function mapDispatchToProps(dispatch) {
  return bindActionCreators({ fetchTodos }, dispatch)
}
  1. 最后,我们连接 App 组件并将其导出:
export default connect(mapStateToProps, mapDispatchToProps)(App)

现在,我们的 App 组件已成功连接到 Redux store。

设置 Provider 组件

最后,我们需要设置一个 Provider 组件,该组件将为 Redux store 提供 context,该 context 将由连接器使用。

现在让我们设置 Provider 组件:

  1. 编辑 src/index.js,并从 react-redux 导入 Provider 组件:
import { Provider } from 'react-redux'
  1. 现在,从 containers 文件夹中导入 ConnectedApp 组件,并导入由 configureStore.js 创建的 Redux store:
import ConnectedApp from './containers/ConnectedApp' import store from './configureStore'
  1. 最后,通过包装 ConnectedApp 组件与 Provider 组件,调整 ReactDOM.render 的第一个参数,如下所示:
ReactDOM.render(
  <Provider store={store}>
    <ConnectedApp />
  </Provider>,
  document.getElementById('root')
)

现在,我们的应用程序将像以前一样工作,但所有内容都连接到 Redux store!正如我们所看到的,与简单使用 React 相比,Redux 需要更多的样板代码,但它有很多优点:

  • 更轻松地处理异步 action(使用 redux-thunk 中间件)
  • 集中式 action 处理(无需在组件中定义 action creator)
  • 用于绑定 action creator 和组合 reducer 的有用函数
  • 减少出错的可能性(例如,通过使用 action 类型,我们可以确保我们没有打错字)

但是 Redux 也有如下所述的缺点:

  • 需要大量样板代码(action 类型、action creator 和连接组件)
  • 在单独的文件中映射 state/action creator(不在需要它们的组件中)

第一点是优势和劣势同时存在;action 类型 和 action creator 确实需要更多的样板代码,但另一方面,它们也让使后阶段更新与 action 相关的代码变得更加容易。第二点,连接组件所需的样板代码,也可以通过使用 Hook 将我们的组件连接到 Redux 来解决。我们将在本章的下一节中使用 Hook 和 Redux。

译者注:在新版 Redux 中,我们可以使用 Redux Toolkit(RTK) 来自动生成 action 类型 和 action creator所需的样板代码。

示例代码

示例代码可以在 Chapter12/chapter12_2 文件夹中找到。

只需运行 npm install 以安装所有依赖项,运行 npm start 即可启动应用程序,然后访问 [http://localhost:3000(如果它没有自动打开)。


(节选)

1

评论 (0)

取消