教程:使用 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
是我们应用的入口组件。它渲染标头、AddTodo
、TodoList
和VisibilityFilters
组件。¥
TodoApp
is the entry component for our app. It renders the header, theAddTodo
,TodoList
, andVisibilityFilters
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 abyIds
map of all todos and aallIds
that contains the list of all ids.visibilityFilters
:一个简单的字符串all
、completed
或incomplete
。¥
visibilityFilters
: A simple stringall
,completed
, orincomplete
.
动作创作者
¥Action Creators
addTodo
创建添加待办事项的操作。它采用单个字符串变量content
并返回ADD_TODO
操作,其中payload
包含自递增id
和content
¥
addTodo
creates the action to add todos. It takes a single string variablecontent
and returns anADD_TODO
action withpayload
containing a self-incrementedid
andcontent
toggleTodo
创建切换待办事项的操作。它采用单个数字变量id
并返回TOGGLE_TODO
操作,其中payload
仅包含id
¥
toggleTodo
creates the action to toggle todos. It takes a single number variableid
and returns aTOGGLE_TODO
action withpayload
containingid
onlysetFilter
创建操作来设置应用的活动过滤器。它采用单个字符串变量filter
并返回SET_FILTER
操作,其中payload
包含filter
本身¥
setFilter
creates the action to set the app’s active filter. It takes a single string variablefilter
and returns aSET_FILTER
action withpayload
containing thefilter
itself
Reducer
todos
reducer将
id
附加到其allIds
字段,并在收到ADD_TODO
操作后在其byIds
字段中设置待办事项¥Appends the
id
to itsallIds
field and sets the todo within itsbyIds
field upon receiving theADD_TODO
action收到
TOGGLE_TODO
操作后切换待办事项的completed
字段¥Toggles the
completed
field for the todo upon receiving theTOGGLE_TODO
action
visibilityFilters
reducer 将其存储切片设置为从SET_FILTER
操作有效负载接收到的新过滤器¥The
visibilityFilters
reducer sets its slice of store to the new filter it receives from theSET_FILTER
action payload
动作类型
¥Action Types
我们使用文件
actionTypes.js
来保存要重用的操作类型常量¥We use a file
actionTypes.js
to hold the constants of action types to be reused
选择器
¥Selectors
getTodoList
从todos
存储返回allIds
列表¥
getTodoList
returns theallIds
list from thetodos
storegetTodoById
在id
给出的存储中找到待办事项¥
getTodoById
finds the todo in the store given byid
getTodos
稍微复杂一些。它从allIds
中取出所有id
,找到byIds
中的每个待办事项,并返回最终的待办事项数组¥
getTodos
is slightly more complex. It takes all theid
s fromallIds
, finds each todo inbyIds
, and returns the final array of todosgetTodosByVisibilityFilter
根据可见性过滤器过滤待办事项¥
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 usedispatch
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。我们从 todos
的 byIds
字段中获得此信息。然而,我们还需要来自存储的 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
如果你同时使用 mapStateToProps
和 mapDispatchToProps
调用 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 />
组件需要能够从存储中读取当前处于活动状态的过滤器,并将操作分派到存储。因此,我们需要同时传递 mapStateToProps
和 mapDispatchToProps
。这里的 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
Reactiflux Redux 通道
¥Reactiflux Redux channel