教程:使用 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
是我们应用程序的入口组件。它渲染标题、AddTodo
、TodoList
和VisibilityFilters
组件。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
: 一个简单的字符串all
、completed
或incomplete
。
- 动作创建者
addTodo
创建添加 todos 的动作。它接受一个字符串变量content
,并返回一个ADD_TODO
动作,其payload
包含自增的id
和content
。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
来保存动作类型的常量,以便重复使用。
- 我们使用一个文件
- 选择器
getTodoList
从todos
存储中返回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。我们从 todos
的 byIds
字段获取这些信息。但是,我们还需要从 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 creators | connect()(Component) | connect(mapStateToProps)(Component) |
注入 action creators | connect(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 的一个非常简单的待办事项应用程序示例。我们所有的组件都已连接!是不是很棒?🎉🎊
链接
获取更多帮助
- Reactiflux Redux 频道
- StackOverflow
- GitHub 问题