Connect API: how to use the legacy connect API">Connect API: how to use the legacy connect API">
跳至主要内容

教程:使用 connect API

提示

我们现在建议使用 React-Redux hooks API 作为默认值。但是,connect API 仍然可以正常工作。

本教程还展示了一些我们不再推荐的旧做法,例如按类型将 Redux 逻辑分成文件夹。为了完整起见,我们保留了本教程,但建议阅读 Redux Essentials 教程Redux 风格指南 在 Redux 文档中了解我们当前的最佳实践。

我们正在开发一个新的教程,将介绍 Hooks API。在此之前,我们建议您阅读 Redux Fundamentals, Part 5: UI and React 以获取 Hooks 教程。

为了展示如何在实践中使用 React Redux,我们将通过创建一个待办事项列表应用程序来演示一个分步示例。

待办事项列表示例

跳转到

React UI 组件

我们已经实现了以下 React UI 组件

  • TodoApp 是我们应用程序的入口组件。它渲染标题、AddTodoTodoListVisibilityFilters 组件。
  • AddTodo 是允许用户输入待办事项并在点击“添加待办事项”按钮后将其添加到列表的组件
    • 它使用受控输入,在 onChange 时设置状态。
    • 当用户点击“添加待办事项”按钮时,它会调度操作(我们将使用 React Redux 提供)以将待办事项添加到存储中。
  • TodoList 是渲染待办事项列表的组件
    • 当选择其中一个 VisibilityFilters 时,它会渲染过滤后的待办事项列表。
  • Todo 是渲染单个待办事项的组件
    • 它渲染待办事项内容,并通过划掉来显示待办事项已完成。
    • 它在 onClick 时调度操作以切换待办事项的完成状态。
  • VisibilityFilters 渲染一组简单的过滤器:全部已完成未完成。点击其中任何一个都会过滤待办事项
    • 它接受来自父级的 activeFilter 属性,该属性指示用户当前选择了哪个过滤器。活动过滤器将用下划线渲染。
    • 它调度 setFilter 操作以更新选定的过滤器。
  • constants 包含我们应用程序的常量数据。
  • 最后,index 将我们的应用程序渲染到 DOM 中。

Redux 存储

应用程序的 Redux 部分已使用 Redux 文档中推荐的模式 设置

  • 存储
    • todos: 一个规范化的 todos reducer。它包含一个 byIds 映射,其中包含所有 todos,以及一个 allIds,其中包含所有 id 的列表。
    • visibilityFilters: 一个简单的字符串 allcompletedincomplete
  • 动作创建者
    • addTodo 创建添加 todos 的动作。它接受一个字符串变量 content,并返回一个 ADD_TODO 动作,其 payload 包含自增的 idcontent
    • toggleTodo 创建切换 todos 的动作。它接受一个数字变量 id,并返回一个 TOGGLE_TODO 动作,其 payload 仅包含 id
    • setFilter 创建设置应用程序活动过滤器的动作。它接受一个字符串变量 filter,并返回一个 SET_FILTER 动作,其 payload 包含 filter 本身。
  • Reducers
    • todos reducer
      • 在接收到 ADD_TODO 动作时,将 id 附加到其 allIds 字段,并将 todo 设置在其 byIds 字段中。
      • 在接收到 TOGGLE_TODO 动作时,切换 todo 的 completed 字段。
    • visibilityFilters reducer 将其存储切片设置为它从 SET_FILTER 动作 payload 中接收的新过滤器。
  • 动作类型
    • 我们使用一个文件 actionTypes.js 来保存动作类型的常量,以便重复使用。
  • 选择器
    • getTodoListtodos 存储中返回 allIds 列表。
    • getTodoById 在存储中找到由 id 给出的 todo。
    • getTodos 稍微复杂一些。它从 allIds 中获取所有 id,在 byIds 中找到每个 todo,并返回最终的 todos 数组。
    • getTodosByVisibilityFilter 根据可见性过滤器过滤 todos。

您可以查看 此 CodeSandbox,了解上面描述的 UI 组件和未连接的 Redux 存储的源代码。


现在我们将展示如何使用 React Redux 将此存储连接到我们的应用程序。

提供存储

首先,我们需要将 store 提供给我们的应用程序。为此,我们将应用程序包装在 React Redux 提供的 <Provider /> API 中。

// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import TodoApp from './TodoApp'

import { Provider } from 'react-redux'
import store from './redux/store'

// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<TodoApp />
</Provider>,
)

请注意,我们的 <TodoApp /> 现在被 <Provider /> 包裹,并以 store 作为属性传递。

连接组件

React Redux 提供了一个 connect 函数,用于从 Redux store 读取值(并在 store 更新时重新读取值)。

connect 函数接受两个参数,都是可选的

  • mapStateToProps:每次 store 状态发生变化时都会调用。它接收整个 store 状态,并应返回此组件所需数据的对象。

  • mapDispatchToProps:此参数可以是函数或对象。

    • 如果它是函数,它将在组件创建时调用一次。它将接收 dispatch 作为参数,并应返回一个包含使用 dispatch 派发操作的函数的对象。
    • 如果它是一个包含操作创建者的对象,则每个操作创建者将被转换为一个属性函数,该函数在调用时会自动派发其操作。注意:我们建议使用这种“对象简写”形式。

通常,您将以这种方式调用 connect

const mapStateToProps = (state, ownProps) => ({
// ... computed data from state and optionally ownProps
})

const mapDispatchToProps = {
// ... normally is an object full of action creators
}

// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)

// We normally do both in one step, like this:
connect(mapStateToProps, mapDispatchToProps)(Component)

让我们先处理 <AddTodo />。它需要触发对 store 的更改以添加新的待办事项。因此,它需要能够向 store 派发操作。以下是我们的操作方法。

我们的 addTodo 操作创建者如下所示

// redux/actions.js
import { ADD_TODO } from './actionTypes'

let nextTodoId = 0
export const addTodo = (content) => ({
type: ADD_TODO,
payload: {
id: ++nextTodoId,
content,
},
})

// ... other actions

将其传递给 connect 后,我们的组件会将其作为属性接收,并且它会在调用时自动派发操作。

// components/AddTodo.js

// ... other imports
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
// ... component implementation
}

export default connect(null, { addTodo })(AddTodo)

请注意,现在 <AddTodo /> 被一个名为 <Connect(AddTodo) /> 的父组件包装。同时,<AddTodo /> 现在获得了一个属性:addTodo 操作。

我们还需要实现 handleAddTodo 函数,以使其派发 addTodo 操作并重置输入

// components/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../redux/actions'

class AddTodo extends React.Component {
// ...

handleAddTodo = () => {
// dispatches actions to add todo
this.props.addTodo(this.state.input)

// sets state back to empty string
this.setState({ input: '' })
}

render() {
return (
<div>
<input
onChange={(e) => this.updateInput(e.target.value)}
value={this.state.input}
/>
<button className="add-todo" onClick={this.handleAddTodo}>
Add Todo
</button>
</div>
)
}
}

export default connect(null, { addTodo })(AddTodo)

现在我们的 <AddTodo /> 已连接到 store。当我们添加待办事项时,它将派发操作以更改 store。我们还没有在应用程序中看到它,因为其他组件尚未连接。如果您已连接 Redux DevTools Extension,您应该会看到操作正在派发

您还应该看到 store 已相应更改

<TodoList /> 组件负责渲染待办事项列表。因此,它需要从 store 读取数据。我们通过使用 mapStateToProps 参数调用 connect 来启用它,该参数是一个描述我们需要从 store 中获取哪些数据的函数。

我们的 <Todo /> 组件接收 todo 项目作为 props。我们从 todosbyIds 字段获取这些信息。但是,我们还需要从 store 的 allIds 字段获取信息,以指示应该渲染哪些 todo 以及它们的顺序。我们的 mapStateToProps 函数可能看起来像这样

// components/TodoList.js

// ...other imports
import { connect } from "react-redux";

const TodoList = // ... UI component implementation

const mapStateToProps = state => {
const { byIds, allIds } = state.todos || {};
const todos =
allIds && allIds.length
? allIds.map(id => (byIds ? { ...byIds[id], id } : null))
: null;
return { todos };
};

export default connect(mapStateToProps)(TodoList);

幸运的是,我们有一个正好可以做到这一点的选择器。我们可以简单地导入选择器并在这里使用它。

// redux/selectors.js

export const getTodosState = (store) => store.todos

export const getTodoList = (store) =>
getTodosState(store) ? getTodosState(store).allIds : []

export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {}

export const getTodos = (store) =>
getTodoList(store).map((id) => getTodoById(store, id))
// components/TodoList.js

// ...other imports
import { connect } from "react-redux";
import { getTodos } from "../redux/selectors";

const TodoList = // ... UI component implementation

export default connect(state => ({ todos: getTodos(state) }))(TodoList);

我们建议将任何复杂的数据查找或计算封装在选择器函数中。此外,您可以使用 Reselect 进一步优化性能,以编写“记忆”选择器,这些选择器可以跳过不必要的操作。(有关为什么以及如何使用选择器函数的更多信息,请参阅 Redux 文档页面上的计算派生数据 和博客文章 惯用 Redux:使用 Reselect 选择器进行封装和性能优化。)

现在我们的 <TodoList /> 已连接到 store。它应该接收 todo 列表,遍历它们,并将每个 todo 传递给 <Todo /> 组件。<Todo /> 将依次将它们渲染到屏幕上。现在尝试添加一个 todo。它应该出现在我们的 todo 列表中!

我们将连接更多组件。在我们这样做之前,让我们先暂停一下,更多地了解 connect

调用 connect 的常见方法

根据您正在使用的组件类型,调用 connect 的方法会有所不同,最常见的方法总结如下

不要订阅 store订阅 store
不要注入 action creatorsconnect()(Component)connect(mapStateToProps)(Component)
注入 action creatorsconnect(null, mapDispatchToProps)(Component)connect(mapStateToProps, mapDispatchToProps)(Component)

不要订阅 store 也不要注入 action creators

如果你调用 `connect` 而不提供任何参数,你的组件将

  • 不会在商店发生变化时重新渲染
  • 接收 `props.dispatch`,你可以使用它手动分发操作
// ... Component
export default connect()(Component) // Component will receive `dispatch` (just like our <TodoList />!)

订阅商店,但不注入操作创建者

如果你只调用 `connect` 带有 `mapStateToProps`,你的组件将

  • 订阅 `mapStateToProps` 从商店中提取的值,并且仅当这些值发生变化时才重新渲染
  • 接收 `props.dispatch`,你可以使用它手动分发操作
// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps)(Component)

不订阅商店,也不注入操作创建者

如果你只调用 `connect` 带有 `mapDispatchToProps`,你的组件将

  • 不会在商店发生变化时重新渲染
  • 接收你使用 `mapDispatchToProps` 注入的每个操作创建者作为 props,并在被调用时自动分发操作
import { addTodo } from './actionCreators'
// ... Component
export default connect(null, { addTodo })(Component)

订阅商店,并注入操作创建者

如果你同时调用 `connect` 带有 `mapStateToProps` 和 `mapDispatchToProps`,你的组件将

  • 订阅 `mapStateToProps` 从商店中提取的值,并且仅当这些值发生变化时才重新渲染
  • 接收你使用 `mapDispatchToProps` 注入的所有操作创建者作为 props,并在被调用时自动分发操作。
import * as actionCreators from './actionCreators'
// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps, actionCreators)(Component)

这四种情况涵盖了 `connect` 的最基本用法。要了解更多关于 `connect` 的信息,请继续阅读我们的 API 部分,它将更详细地解释它。


现在让我们连接剩下的 `<TodoApp />`。

我们应该如何实现切换待办事项的交互?一位敏锐的读者可能已经有了答案。如果你已经设置好环境并一直遵循到目前为止的步骤,现在是将它放在一边并自己实现该功能的好时机。毫无疑问,我们将以类似的方式连接我们的 `<Todo />` 来分发 `toggleTodo`

// components/Todo.js

// ... other imports
import { connect } from "react-redux";
import { toggleTodo } from "../redux/actions";

const Todo = // ... component implementation

export default connect(
null,
{ toggleTodo }
)(Todo);

现在我们的待办事项可以被切换为已完成。我们快到了!

最后,让我们实现 `VisibilityFilters` 功能。

`<VisibilityFilters />` 组件需要能够从商店中读取当前活动的过滤器,并向商店分发操作。因此,我们需要传递 `mapStateToProps` 和 `mapDispatchToProps`。这里的 `mapStateToProps` 可以是 `visibilityFilter` 状态的简单访问器。而 `mapDispatchToProps` 将包含 `setFilter` 操作创建者。

// components/VisibilityFilters.js

// ... other imports
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";

const VisibilityFilters = // ... component implementation

const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);

同时,我们还需要更新我们的 `<TodoList />` 组件,以根据活动过滤器筛选待办事项。之前我们传递给 `<TodoList />` `connect` 函数调用的 `mapStateToProps` 仅仅是选择整个待办事项列表的选择器。让我们编写另一个选择器来帮助根据待办事项的状态进行筛选。

// redux/selectors.js

// ... other selectors
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
const allTodos = getTodos(store)
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
return allTodos.filter((todo) => todo.completed)
case VISIBILITY_FILTERS.INCOMPLETE:
return allTodos.filter((todo) => !todo.completed)
case VISIBILITY_FILTERS.ALL:
default:
return allTodos
}
}

并使用选择器的帮助连接到商店

// components/TodoList.js

// ...

const mapStateToProps = (state) => {
const { visibilityFilter } = state
const todos = getTodosByVisibilityFilter(state, visibilityFilter)
return { todos }
}

export default connect(mapStateToProps)(TodoList)

现在我们已经完成了使用 React Redux 的一个非常简单的待办事项应用程序示例。我们所有的组件都已连接!是不是很棒?🎉🎊

获取更多帮助