Store
在前面的章节中,我们定义了代表“发生了什么”的事实的actions以及根据这些动作更新状态的Reducer。
Store是将他们聚集在一起的对象。Store有以下责任:
- 持有应用程序状态;
- 允许通过getState()访问状态;
- 允许状态通过dispatch(action)更新;
- 通过订阅注册subscribe(listener);
- 通过subscribe(listener)返回的函数来处理注销监听器。
重要的是要注意,你将只有一个store在Redux应用程序中。当你想拆分你的数据处理逻辑时,你将使用reducer组合而不是许多store。
如果您有reducer,创建store很容易。在上一节中,我们使用了combineReducers()将多个reducer合并为一个。我们现在将其导入,并将其传递给createStore()。
1 |
import { createStore } from 'redux' |
您可以选择指定初始状态作为createStore()的第二个参数。这对于保证客户端的状态与服务器上运行的Redux应用程序的状态相匹配非常有用。
1 |
const store = createStore(todoApp, window.STATE_FROM_SERVER) |
Dispatching Actions
现在我们已经创建了一个store,让我们来验证我们的程序的作品!即使没有任何UI,我们也可以测试更新逻辑。
1 |
import { |
你可以看到这是如何导致store的状态发生变化的:
在我们开始编写UI之前,我们指定了我们的应用程序的行为。我们不会在本教程中做到这一点,但是现在您可以为您的reducer和actionCreator编写测试。你不需要嘲笑任何东西,因为它们只是纯粹的功能。调用他们,并就他们返回的内容作出断言。
index.js
1 |
import { createStore } from 'redux' |
Data Flow
Redux体系结构围绕严格的单向数据流进行。
这意味着应用程序中的所有数据都遵循相同的生命周期模式,使您的应用程序的逻辑更具可预测性并更易于理解。它还鼓励数据规范化,以便最终不会出现多个相互不知道的相同数据的独立副本。
如果您仍然不确定,请阅读Motivation和The Case for Flux,以获得支持单向数据流的引人注目的论点。虽然Redux不完全是Flux,但它具有相同的关键优势。
任何Redux应用程序中的数据生命周期都遵循以下4个步骤:
1.你可以调用store.dispatch(action)
动作是一个描述发生的事情的简单对象。例如:
1 |
{ type: 'LIKE_ARTICLE', articleId: 42 } |
把行动看作是一个非常简短的新闻片段。“玛丽喜欢第42条”或“阅读Redux文档”。被添加到todos列表中。“
您可以从应用程序中的任何位置调用store.dispatch(action),包括组件和XHR回调,或者甚至以预定的时间间隔。
2.Redux store调用您提供的reducer功能。
store将向reducer传递两个参数:当前状态树和动作。例如,在todo应用程序中,根reducer可能会收到如下所示的内容:
1 |
// The current application state (list of todos and chosen filter) |
请注意,reducer是一个纯功能。它只计算下一个状态。它应该是完全可预测的:多次调用相同的输入应该产生相同的输出。它不应该执行API调用或路由器转换等任何副作用。这些应该在动作发出之前发生。
3.根reducer可以将多个reducer的输出组合成单个状态树。
您如何构建根部reducer完全取决于您。Redux提供了combineReducers()辅助函数,用于将根reducer“分解”为单独的函数,每个函数管理状态树的一个分支。
这是combineReducers()的工作原理。假设您有两个reducer,一个用于todos列表,另一个用于当前选择的过滤器设置:
1 |
function (state = [], action) { |
当你发出一个动作时,由combineReducers返回的todoApp将调用两个reducer:
1 |
let nextTodos = todos(state.todos, action) |
然后它会将两组结果合并为一个状态树:
1 |
return { |
虽然combineReducers()是一个方便的助手实用程序,但您不必使用它;随时写你自己的根reducer!
4.Redux存储保存由根reducer返回的完整状态树。
这棵新树现在是你的应用程序的下一个状态!现在每个用store.subscribe(listener)注册的监听器都会被调用;监听器可以调用store.getState()来获取当前状态。
现在,UI可以更新以反映新的状态。如果使用React Redux之类的绑定,则这是调用component.setState(newState)的点。
React
从一开始,我们需要强调Redux与React没有任何关系。您可以使用React,Angular,Ember,jQuery或vanilla JavaScript编写Redux应用程序。
也就是说,Redux与React和Deku等库合作得非常好,因为它们让您将UI描述为状态的函数,Redux响应操作发出状态更新。
我们将使用React来构建我们简单的待办事项应用程序。
React Redux install
React绑定不包含在默认的Redux中。你需要明确地安装它们:
1 |
npm install --save react-redux |
如果您不使用npm,您可以从unpkg获取最新的UMD版本(开发版或生产版)。如果您通过script标记将其添加到您的页面,则UMD构建会导出一个名为window.ReactRedux的全局。
展示和容器组件
Redux的React绑定包含分离表示和容器组件的想法。如果您不熟悉这些条款,请先阅读有关条款,然后再回来。他们很重要,所以我们会等待!
完成阅读文章?我们来重述一下他们的区别:
展示 Components | 容器 Components | |
---|---|---|
含义 | 看起来如何(标记,样式) | 工作原理(数据读取,状态更新) |
Redux | No |
Yes |
读取数据 | 从props读取数据 | 订阅Redux状态 |
改变数据 | 从props调用回调 | Dispatch Redux actions |
写入 | 手动写 | 通常由React Redux生成 |
我们要编写的大多数组件都是展示的,但我们需要生成一些容器组件以将它们连接到Redux存储。这和下面的设计概要并不意味着容器组件必须靠近组件树的顶部。如果一个容器组件变得太复杂了(即它有很多嵌套的展示组件,并有无数的回调被传递下去),那么在组件树中引入另一个容器,如FAQ中所述。
从技术上讲,你可以用store.subscribe()手工编写容器组件。我们不建议您这样做,因为React Redux会进行许多难以完成的性能优化。出于这个原因,我们将使用React Redux提供的connect()函数生成它们,而不是编写容器组件,如下所示。
设计组件层次结构
请记住我们如何设计根状态对象的形状?是时候我们设计UI层次结构来匹配它。这不是特定于Redux的任务。在React中思考是一个很好的教程,可以解释这个过程。
我们的设计简介很简单。我们想要显示待办事项列表。单击时,待办事项完成后划掉。我们想要显示一个字段,用户可以添加新的待办事项。在底部,我们想要显示切换显示全部,只显示完成或仅显示活动待办事项。
设计演示组件
我看到以下演示组件和它们的props出现在这个简短的介绍中:
- ToDoList 是一个列表,显示可见的待办事项。
- todos:Array是包含{id,text,completed}形状的todo项目的数组。
- onTodoClick(id:number)是单击todo时调用的回调。
- ToDo是一个单一的待办事项
- text: string是要显示的文字。
- completed: boolean是待办事项是否应该出现划掉。
- onClick()是单击todo时调用的回调。
- Link是一个回调链接。
- onClick()是单击链接时调用的回调。
- Footer是我们让用户更改当前可见的待办事项的地方。
- App是呈现其他所有内容的根组件。
他们描述的外观,但不知道数据来自哪里,或者如何改变它。他们只渲染给他们的东西。如果你从Redux迁移到其他的东西,你将能够保持所有这些组件完全一样。他们没有依赖Redux。
设计容器组件
我们还需要一些容器组件将演示组件连接到Redux。例如,演示TodoList组件需要一个容器,如VisibleTodoList,该容器订阅Redux存储并知道如何应用当前可见性过滤器。要更改可见性过滤器,我们将提供一个FilterLink容器组件,用于呈现链接,并在点击时分配适当的操作:
- VisibleTodoList根据当前可见性过滤器过滤待办事项并呈现TodoList。
- FilterLink获取当前可见性过滤器并呈现链接。
- filter: string是它代表的可见性过滤器。
设计其他组件
有时很难判断某个组件应该是一个表示组件还是一个容器。例如,有时窗体和函数实际上是耦合在一起的,比如在这个小部件的情况下:
- AddTodo是一个带有“添加”按钮的输入字段
从技术上讲,我们可以将它分成两个部分,但现阶段可能为时过早。在非常小的组件中混合表示和逻辑是很好的。随着它的增长,如何分割它将会更加明显,所以我们会把它混合起来。
实现组件
我们来编写组件!我们从演示组件开始,所以我们不需要考虑绑定到Redux。
实现演示组件
这些都是普通的React组件,所以我们不会详细检查它们。我们编写功能无状态的组件,除非我们需要使用本地状态或生命周期方法。这并不意味着表示组件必须是功能 - 这样更容易定义它们。如果您需要添加本地状态,生命周期方法或性能优化,则可以将它们转换为类。components/Todo.js
1 |
import React from 'react' |
components/TodoList.js
1 |
import React from 'react' |
components/Link.js
1 |
|
components/Footer.js
1 |
|
实现容器组件
现在是时候通过创建一些容器将这些表示性组件连接到Redux。从技术上讲,容器组件只是一个React组件,它使用store.subscribe()来读取Redux状态树的一部分,并为它呈现的呈现组件提供props。您可以手动编写容器组件,但我们建议使用React Redux库的connect()函数生成容器组件,该函数提供许多有用的优化以防止不必要的重新呈现。(这样做的一个结果是你不必担心自己实现shouldComponentUpdate的React性能建议。)
要使用connect(),需要定义一个名为mapStateToProps的特殊函数,它告诉如何将当前的Redux存储状态转换为要传递给要包装的表示组件的道具。例如,VisibleTodoList需要计算待办事项以传递给TodoList,所以我们定义一个函数,根据state.visibilityFilter过滤state.todos,并在其mapStateToProps中使用它:
1 |
const getVisibleTodos = (todos, filter) => { |
除了读取状态之外,容器组件还可以调度操作。以类似的方式,您可以定义一个名为mapDispatchToProps()的函数,它接收dispatch()方法并返回要注入演示组件的回调支持。例如,我们希望VisibleTodoList将名为onTodoClick的道具注入TodoList组件,并且我们希望onTodoClick发送TOGGLE_TODO操作:
1 |
const mapDispatchToProps = dispatch => { |
最后,我们通过调用connect()并传递这两个函数来创建VisibleTodoList:
1 |
import { connect } from 'react-redux' |
这些是React Redux API的基础知识,但有几个捷径和开关选项,因此我们鼓励您详细查看其文档。如果您担心mapStateToProps过于频繁地创建新对象,则可能需要了解使用重新选择计算派生数据。
查找下面定义的其余容器组件:container/FilterLink.js
1 |
import { connect } from 'react-redux' |
containers/VisibleTodoList.js
1 |
import { connect } from 'react-redux' |
实现其他组件
containers/AddTodo.js
如前所述,AddTodo组件的展示和逻辑都被混合到一个单一的定义中。
1 |
import React from 'react' |
如果您不熟悉ref属性,请阅读本文档以熟悉推荐使用此属性。
将容器捆绑在一个组件内
components/App.js
1 |
import React from 'react' |
传递Store
所有容器组件都需要访问Redux存储,以便他们可以订阅它。一种选择是将其作为props传递给每个容器组件。然而它很乏味,因为即使是通过表示组件,也只是因为它们碰巧在组件树中渲染容器,所以你必须连线存储。
我们推荐的选项是使用名为
index.js
1 |
import React from 'react' |