Skip to main content

教程:使用 connect API

¥Tutorial: Using the connect API

提示

我们现在推荐使用 React-Redux hooks API 作为默认值。不过,connect API 仍然可以正常工作。

¥We now recommend using the React-Redux hooks API as the default. However, the connect API still works fine.

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

¥This tutorial also shows some older practices we no longer recommend, like separating Redux logic into folders by type. We've kept this tutorial as-is for completeness, but recommend reading through the "Redux Essentials" tutorial and the Redux Style Guide in the Redux docs for our current best practices.

我们正在编写一个新教程,将介绍 hooks API。在那之前,我们建议阅读 Redux 基础知识,第 5 部分:用户界面和反应 的 Hooks 教程。

¥We're working on a new tutorial that will introduce the hooks APIs. Until then, we suggest reading Redux Fundamentals, Part 5: UI and React for a hooks tutorial.

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

¥To see how to use React Redux in practice, we’ll show a step-by-step example by creating a todo list app.

待办事项列表示例

¥A Todo List Example

跳到

¥Jump to

React UI 组件

¥The React UI Components

我们已经实现了 React UI 组件,如下所示:

¥We have implemented our React UI components as follows:

  • TodoApp 是我们应用的入口组件。它渲染标头、AddTodoTodoListVisibilityFilters 组件。

    ¥TodoApp is the entry component for our app. It renders the header, the AddTodo, TodoList, and VisibilityFilters components.

  • AddTodo 是允许用户输入待办事项并在单击“添加待办事项”按钮后添加到列表中的组件:

    ¥AddTodo is the component that allows a user to input a todo item and add to the list upon clicking its “Add Todo” button:

    • 它使用一个受控输入来设置 onChange 的状态。

      ¥It uses a controlled input that sets state upon onChange.

    • 当用户单击“添加待办事项”按钮时,它会调度操作(我们将使用 React Redux 提供)以将待办事项添加到存储。

      ¥When the user clicks on the “Add Todo” button, it dispatches the action (that we will provide using React Redux) to add the todo to the store.

  • TodoList 是渲染待办事项列表的组件:

    ¥TodoList is the component that renders the list of todos:

    • 当选择 VisibilityFilters 之一时,它会渲染已过滤的待办事项列表。

      ¥It renders the filtered list of todos when one of the VisibilityFilters is selected.

  • Todo 是渲染单个待办事项的组件:

    ¥Todo is the component that renders a single todo item:

    • 它渲染待办事项内容,并通过划掉它来显示待办事项已完成。

      ¥It renders the todo content, and shows that a todo is completed by crossing it out.

    • 它在 onClick 时调度操作以切换待办事项的完成状态。

      ¥It dispatches the action to toggle the todo's complete status upon onClick.

  • VisibilityFilters 渲染一组简单的过滤器:全部、已完成和未完成。单击其中每一项都会过滤待办事项:

    ¥VisibilityFilters renders a simple set of filters: all, completed, and incomplete. Clicking on each one of them filters the todos:

    • 它接受来自父级的 activeFilter 属性,指示用户当前选择了哪个过滤器。活动过滤器用下划线表示。

      ¥It accepts an activeFilter prop from the parent that indicates which filter is currently selected by the user. An active filter is rendered with an underscore.

    • 它调度 setFilter 操作来更新选定的过滤器。

      ¥It dispatches the setFilter action to update the selected filter.

  • constants 保存我们应用的常量数据。

    ¥constants holds the constants data for our app.

  • 最后 index 将我们的应用渲染到 DOM。

    ¥And finally index renders our app to the DOM.


Redux 存储

¥The Redux Store

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

¥The Redux portion of the application has been set up using the patterns recommended in the Redux docs:

  • 存储

    ¥Store

    • todos:todos 的标准化缩减器。它包含所有待办事项的 byIds 映射和包含所有 id 列表的 allIds

      ¥todos: A normalized reducer of todos. It contains a byIds map of all todos and a allIds that contains the list of all ids.

    • visibilityFilters:一个简单的字符串 allcompletedincomplete

      ¥visibilityFilters: A simple string all, completed, or incomplete.

  • 动作创作者

    ¥Action Creators

    • addTodo 创建添加待办事项的操作。它采用单个字符串变量 content 并返回 ADD_TODO 操作,其中 payload 包含自递增 idcontent

      ¥addTodo creates the action to add todos. It takes a single string variable content and returns an ADD_TODO action with payload containing a self-incremented id and content

    • toggleTodo 创建切换待办事项的操作。它采用单个数字变量 id 并返回 TOGGLE_TODO 操作,其中 payload 仅包含 id

      ¥toggleTodo creates the action to toggle todos. It takes a single number variable id and returns a TOGGLE_TODO action with payload containing id only

    • setFilter 创建操作来设置应用的活动过滤器。它采用单个字符串变量 filter 并返回 SET_FILTER 操作,其中 payload 包含 filter 本身

      ¥setFilter creates the action to set the app’s active filter. It takes a single string variable filter and returns a SET_FILTER action with payload containing the filter itself

  • Reducer

    • todos reducer

      • id 附加到其 allIds 字段,并在收到 ADD_TODO 操作后在其 byIds 字段中设置待办事项

        ¥Appends the id to its allIds field and sets the todo within its byIds field upon receiving the ADD_TODO action

      • 收到 TOGGLE_TODO 操作后切换待办事项的 completed 字段

        ¥Toggles the completed field for the todo upon receiving the TOGGLE_TODO action

    • visibilityFilters reducer 将其存储切片设置为从 SET_FILTER 操作有效负载接收到的新过滤器

      ¥The visibilityFilters reducer sets its slice of store to the new filter it receives from the SET_FILTER action payload

  • 动作类型

    ¥Action Types

    • 我们使用文件 actionTypes.js 来保存要重用的操作类型常量

      ¥We use a file actionTypes.js to hold the constants of action types to be reused

  • 选择器

    ¥Selectors

    • getTodoListtodos 存储返回 allIds 列表

      ¥getTodoList returns the allIds list from the todos store

    • getTodoByIdid 给出的存储中找到待办事项

      ¥getTodoById finds the todo in the store given by id

    • getTodos 稍微复杂一些。它从 allIds 中取出所有 id,找到 byIds 中的每个待办事项,并返回最终的待办事项数组

      ¥getTodos is slightly more complex. It takes all the ids from allIds, finds each todo in byIds, and returns the final array of todos

    • getTodosByVisibilityFilter 根据可见性过滤器过滤待办事项

      ¥getTodosByVisibilityFilter filters the todos according to the visibility filter

你可以查看 这个代码沙盒 来获取上述 UI 组件和未连接的 Redux 存储的源代码。

¥You may check out this CodeSandbox for the source code of the UI components and the unconnected Redux store described above.


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

¥We will now show how to connect this store to our app using React Redux.

提供存储

¥Providing the Store

首先,我们需要使 store 可用于我们的应用。为此,我们使用 React Redux 提供的 <Provider /> API 封装我们的应用。

¥First we need to make the store available to our app. To do this, we wrap our app with the <Provider /> API provided by React Redux.

// 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 作为 prop 传入。

¥Notice how our <TodoApp /> is now wrapped with the <Provider /> with store passed in as a prop.

连接组件

¥Connecting the Components

React Redux 提供了 connect 函数,供你从 Redux 存储中读取值(并在存储更新时重新读取值)。

¥React Redux provides a connect function for you to read values from the Redux store (and re-read the values when the store updates).

connect 函数有两个参数,都是可选的:

¥The connect function takes two arguments, both optional:

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

    ¥mapStateToProps: called every time the store state changes. It receives the entire store state, and should return an object of data this component needs.

  • mapDispatchToProps:该参数可以是函数,也可以是对象。

    ¥mapDispatchToProps: this parameter can either be a function, or an object.

    • 如果它是一个函数,它将在组件创建时被调用一次。它将接收 dispatch 作为参数,并且应该返回一个充满使用 dispatch 来调度操作的函数的对象。

      ¥If it’s a function, it will be called once on component creation. It will receive dispatch as an argument, and should return an object full of functions that use dispatch to dispatch actions.

    • 如果它是一个充满动作创建者的对象,则每个动作创建者将变成一个 prop 函数,在调用时自动调度其动作。注意:我们建议使用这种“对象速记”形式。

      ¥If it’s an object full of action creators, each action creator will be turned into a prop function that automatically dispatches its action when called. Note: We recommend using this “object shorthand” form.

通常,你会这样调用 connect

¥Normally, you’ll call connect in this way:

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 的更改以添加新的待办事项。因此,它需要能够对存储进行 dispatch 次操作。我们是这样做的。

¥Let’s work on <AddTodo /> first. It needs to trigger changes to the store to add new todos. Therefore, it needs to be able to dispatch actions to the store. Here’s how we do it.

我们的 addTodo 动作创建器如下所示:

¥Our addTodo action creator looks like this:

// 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,我们的组件将其作为 prop 接收,并且在调用时会自动分派该操作。

¥By passing it to connect, our component receives it as a prop, and it will automatically dispatch the action when it’s called.

// 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 行动。

¥Notice now that <AddTodo /> is wrapped with a parent component called <Connect(AddTodo) />. Meanwhile, <AddTodo /> now gains one prop: the addTodo action.

我们还需要实现 handleAddTodo 函数,让它分派 addTodo 操作并重置输入

¥We also need to implement the handleAddTodo function to let it dispatch the addTodo action and reset the input

// 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 /> 已经连接到存储了。当我们添加待办事项时,它会调度一个操作来更改存储。我们没有在应用中看到它,因为其他组件尚未连接。如果你连接了 Redux DevTools 扩展,你应该会看到正在分派的操作:

¥Now our <AddTodo /> is connected to the store. When we add a todo it would dispatch an action to change the store. We are not seeing it in the app because the other components are not connected yet. If you have the Redux DevTools Extension hooked up, you should see the action being dispatched:

你还应该看到存储也发生了相应的变化:

¥You should also see that the store has changed accordingly:

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

¥The <TodoList /> component is responsible for rendering the list of todos. Therefore, it needs to read data from the store. We enable it by calling connect with the mapStateToProps parameter, a function describing which part of the data we need from the store.

我们的 <Todo /> 组件将待办事项作为 props。我们从 todosbyIds 字段中获得此信息。然而,我们还需要来自存储的 allIds 字段的信息,指示哪些待办事项以及它们应该渲染的顺序。我们的 mapStateToProps 函数可能如下所示:

¥Our <Todo /> component takes the todo item as props. We have this information from the byIds field of the todos. However, we also need the information from the allIds field of the store indicating which todos and in what order they should be rendered. Our mapStateToProps function may look like this:

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

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

¥Luckily we have a selector that does exactly this. We may simply import the selector and use it here.

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

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

¥We recommend encapsulating any complex lookups or computations of data in selector functions. In addition, you can further optimize the performance by using Reselect to write “memoized” selectors that can skip unnecessary work. (See the Redux docs page on Computing Derived Data and the blog post Idiomatic Redux: Using Reselect Selectors for Encapsulation and Performance for more information on why and how to use selector functions.)

现在我们的 <TodoList /> 已连接到存储。它应该接收待办事项列表,映射它们,并将每个待办事项传递给 <Todo /> 组件。<Todo /> 将依次将它们渲染到屏幕上。现在尝试添加待办事项。它应该出现在我们的待办事项列表中!

¥Now that our <TodoList /> is connected to the store. It should receive the list of todos, map over them, and pass each todo to the <Todo /> component. <Todo /> will in turn render them to the screen. Now try adding a todo. It should come up on our todo list!

我们将连接更多组件。在此之前,我们先暂停一下,先了解一下 connect

¥We will connect more components. Before we do this, let’s pause and learn a bit more about connect first.

调用 connect 的常见方式

¥Common ways of calling connect

根据你使用的组件类型,调用 connect 的方式有多种,最常见的方式总结如下:

¥Depending on what kind of components you are working with, there are different ways of calling connect , with the most common ones summarized as below:

不要订阅存储订阅存储
不要注入动作创建者connect()(Component)connect(mapStateToProps)(Component)
注入动作创作者connect(null, mapDispatchToProps)(Component)connect(mapStateToProps, mapDispatchToProps)(Component)

不订阅存储,不注入动作创建者

¥Do not subscribe to the store and do not inject action creators

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

¥If you call connect without providing any arguments, your component will:

  • 存储更改时不重新渲染

    ¥not re-render when the store changes

  • 接收 props.dispatch,你可以使用它来手动调度操作

    ¥receive props.dispatch that you may use to manually dispatch action

// ... Component
export default connect()(Component) // Component will receive `dispatch` (just like our <TodoList />!)

订阅存储并且不注入动作创建者

¥Subscribe to the store and do not inject action creators

如果你仅使用 mapStateToProps 调用 connect,你的组件将:

¥If you call connect with only mapStateToProps, your component will:

  • 订阅 mapStateToProps 从存储中提取的值,并仅在这些值发生更改时重新渲染

    ¥subscribe to the values that mapStateToProps extracts from the store, and re-render only when those values have changed

  • 接收 props.dispatch,你可以使用它来手动调度操作

    ¥receive props.dispatch that you may use to manually dispatch action

// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps)(Component)

不要订阅存储并注入动作创建者

¥Do not subscribe to the store and inject action creators

如果你仅使用 mapDispatchToProps 调用 connect,你的组件将:

¥If you call connect with only mapDispatchToProps, your component will:

  • 存储更改时不重新渲染

    ¥not re-render when the store changes

  • 接收你使用 mapDispatchToProps 作为 props 注入的每个动作创建者,并在被调用时自动调度动作

    ¥receive each of the action creators you inject with mapDispatchToProps as props and automatically dispatch the actions upon being called

import { addTodo } from './actionCreators'
// ... Component
export default connect(null, { addTodo })(Component)

订阅存储并注入动作创建者

¥Subscribe to the store and inject action creators

如果你同时使用 mapStateToPropsmapDispatchToProps 调用 connect,你的组件将:

¥If you call connect with both mapStateToProps and mapDispatchToProps, your component will:

  • 订阅 mapStateToProps 从存储中提取的值,并仅在这些值发生更改时重新渲染

    ¥subscribe to the values that mapStateToProps extracts from the store, and re-render only when those values have changed

  • 接收你使用 mapDispatchToProps 作为 props 注入的所有动作创建者,并在被调用时自动分派动作。

    ¥receive all of the action creators you inject with mapDispatchToProps as props and automatically dispatch the actions upon being called.

import * as actionCreators from './actionCreators'
// ... Component
const mapStateToProps = (state) => state.partOfState
export default connect(mapStateToProps, actionCreators)(Component)

这四个案例涵盖了 connect 最基本的用法。要了解有关 connect 的更多信息,请继续阅读我们的 API 部分,其中有更详细的解释。

¥These four cases cover the most basic usages of connect. To read more about connect, continue reading our API section that explains it in more detail.


现在让我们连接 <TodoApp /> 的其余部分。

¥Now let’s connect the rest of our <TodoApp />.

我们应该如何实现切换待办事项的交互呢?敏锐的读者可能已经有了答案。如果你已经设置了环境并一直跟进到这一点,那么现在是将其放在一边并自行实现该功能的好时机。毫不奇怪,我们以类似的方式连接 <Todo /> 来调度 toggleTodo

¥How should we implement the interaction of toggling todos? A keen reader might already have an answer. If you have your environment set up and have followed through up until this point, now is a good time to leave it aside and implement the feature by yourself. There would be no surprise that we connect our <Todo /> to dispatch toggleTodo in a similar way:

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

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

¥Now our todo’s can be toggled complete. We’re almost there!

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

¥Finally, let’s implement our VisibilityFilters feature.

<VisibilityFilters /> 组件需要能够从存储中读取当前处于活动状态的过滤器,并将操作分派到存储。因此,我们需要同时传递 mapStateToPropsmapDispatchToProps。这里的 mapStateToProps 可以是 visibilityFilter 状态的简单访问器。mapDispatchToProps 将包含 setFilter 动作创建器。

¥The <VisibilityFilters /> component needs to be able to read from the store which filter is currently active, and dispatch actions to the store. Therefore, we need to pass both a mapStateToProps and mapDispatchToProps. The mapStateToProps here can be a simple accessor of the visibilityFilter state. And the mapDispatchToProps will contain the setFilter action creator.

// 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 只是选择整个待办事项列表的选择器。让我们编写另一个选择器来帮助按状态过滤待办事项。

¥Meanwhile, we also need to update our <TodoList /> component to filter todos according to the active filter. Previously the mapStateToProps we passed to the <TodoList /> connect function call was simply the selector that selects the whole list of todos. Let’s write another selector to help filtering todos by their status.

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

并在选择器的帮助下连接到存储:

¥And connecting to the store with the help of the selector:

// components/TodoList.js

// ...

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

export default connect(mapStateToProps)(TodoList)

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

¥Now we've finished a very simple example of a todo app with React Redux. All our components are connected! Isn't that nice? 🎉🎊

¥Links

获得更多帮助

¥Get More Help