Redux Store以及数据流

北战南征 提交于 2019-12-01 18:57:10

Store

在前面的章节中,我们定义了代表“发生了什么”的事实的actions以及根据这些动作更新状态的Reducer。

Store是将他们聚集在一起的对象。Store有以下责任:

  • 持有应用程序状态;
  • 允许通过getState()访问状态;
  • 允许状态通过dispatch(action)更新;
  • 通过订阅注册subscribe(listener);
  • 通过subscribe(listener)返回的函数来处理注销监听器。

重要的是要注意,你将只有一个store在Redux应用程序中。当你想拆分你的数据处理逻辑时,你将使用reducer组合而不是许多store。
如果您有reducer,创建store很容易。在上一节中,我们使用了combineReducers()将多个reducer合并为一个。我们现在将其导入,并将其传递给createStore()。

123
import { createStore } from 'redux'import todoApp from './reducers'const store = createStore(todoApp)

您可以选择指定初始状态作为createStore()的第二个参数。这对于保证客户端的状态与服务器上运行的Redux应用程序的状态相匹配非常有用。

1
const store = createStore(todoApp, window.STATE_FROM_SERVER)

Dispatching Actions

现在我们已经创建了一个store,让我们来验证我们的程序的作品!即使没有任何UI,我们也可以测试更新逻辑。

1234567891011121314151617181920212223242526
import {  addTodo,  toggleTodo,  setVisibilityFilter,  VisibilityFilters} from './actions'console.log(store.getState())// Every time the state changes, log it// Note that subscribe() returns a function for unregistering the listenerconst unsubscribe = store.subscribe(() =>  console.log(store.getState()))// Dispatch some actionsstore.dispatch(addTodo('Learn about actions'))store.dispatch(addTodo('Learn about reducers'))store.dispatch(addTodo('Learn about store'))store.dispatch(toggleTodo(0))store.dispatch(toggleTodo(1))store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))// Stop listening to state updatesunsubscribe()

你可以看到这是如何导致store的状态发生变化的:


在我们开始编写UI之前,我们指定了我们的应用程序的行为。我们不会在本教程中做到这一点,但是现在您可以为您的reducer和actionCreator编写测试。你不需要嘲笑任何东西,因为它们只是纯粹的功能。调用他们,并就他们返回的内容作出断言。

index.js

1234
import { createStore } from 'redux'import todoApp from './reducers'const store = createStore(todoApp)

Data Flow

Redux体系结构围绕严格的单向数据流进行。
这意味着应用程序中的所有数据都遵循相同的生命周期模式,使您的应用程序的逻辑更具可预测性并更易于理解。它还鼓励数据规范化,以便最终不会出现多个相互不知道的相同数据的独立副本。
如果您仍然不确定,请阅读Motivation和The Case for Flux,以获得支持单向数据流的引人注目的论点。虽然Redux不完全是Flux,但它具有相同的关键优势。
任何Redux应用程序中的数据生命周期都遵循以下4个步骤:
1.你可以调用store.dispatch(action)
动作是一个描述发生的事情的简单对象。例如:

123
{ type: 'LIKE_ARTICLE', articleId: 42 } { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } } { type: 'ADD_TODO', text: 'Read the Redux docs.' }

把行动看作是一个非常简短的新闻片段。“玛丽喜欢第42条”或“阅读Redux文档”。被添加到todos列表中。“
您可以从应用程序中的任何位置调用store.dispatch(action),包括组件和XHR回调,或者甚至以预定的时间间隔。
2.Redux store调用您提供的reducer功能。
store将向reducer传递两个参数:当前状态树和动作。例如,在todo应用程序中,根reducer可能会收到如下所示的内容:

12345678910111213141516171819
// The current application state (list of todos and chosen filter) let previousState = {   visibleTodoFilter: 'SHOW_ALL',   todos: [     {       text: 'Read the docs.',       complete: false     }   ] } // The action being performed (adding a todo) let action = {   type: 'ADD_TODO',   text: 'Understand the flow.' } // Your reducer returns the next application state let nextState = todoApp(previousState, action)

请注意,reducer是一个纯功能。它只计算下一个状态。它应该是完全可预测的:多次调用相同的输入应该产生相同的输出。它不应该执行API调用或路由器转换等任何副作用。这些应该在动作发出之前发生。
3.根reducer可以将多个reducer的输出组合成单个状态树。
您如何构建根部reducer完全取决于您。Redux提供了combineReducers()辅助函数,用于将根reducer“分解”为单独的函数,每个函数管理状态树的一个分支。
这是combineReducers()的工作原理。假设您有两个reducer,一个用于todos列表,另一个用于当前选择的过滤器设置:

1234567891011121314
function (state = [], action) {  // Somehow calculate it...  return nextState}function visibleTodoFilter(state = 'SHOW_ALL', action) {  // Somehow calculate it...  return nextState}let todoApp = combineReducers({  todos,  visibleTodoFilter})

当你发出一个动作时,由combineReducers返回的todoApp将调用两个reducer:

12
let nextTodos = todos(state.todos, action)let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)

然后它会将两组结果合并为一个状态树:

1234
 return {  todos: nextTodos,  visibleTodoFilter: nextVisibleTodoFilter}

虽然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

123456789101112131415161718192021
import React from 'react'import PropTypes from 'prop-types'const Todo = ({ onClick, completed, text }) => (  <li    onClick={onClick}    style={ {      textDecoration: completed ? 'line-through' : 'none'    }}  >    {text}  </li>)Todo.propTypes = {  onClick: PropTypes.func.isRequired,  completed: PropTypes.bool.isRequired,  text: PropTypes.string.isRequired}export default Todo

components/TodoList.js

123456789101112131415161718192021222324
import React from 'react'import PropTypes from 'prop-types'import Todo from './Todo'const TodoList = ({ todos, onTodoClick }) => (  <ul>    {todos.map((todo, index) => (      <Todo key={index} {...todo} onClick={() => onTodoClick(index)} />    ))}  </ul>)TodoList.propTypes = {  todos: PropTypes.arrayOf(    PropTypes.shape({      id: PropTypes.number.isRequired,      completed: PropTypes.bool.isRequired,      text: PropTypes.string.isRequired    }).isRequired  ).isRequired,  onTodoClick: PropTypes.func.isRequired}export default TodoList

components/Link.js

大专栏  Redux Store以及数据流
1234567891011121314151617181920212223242526272829
importimport  ReactReact  fromfrom  ''reactreact'' importimport  PropTypesPropTypes  fromfrom  ''prop-typesprop-types''  constconst  LinkLink  == ({ active, children, onClick })  ({ active, childr => {  if (active) {    return <span>{children}</span>  }  return (    <a      href=""      onClick={e => {        e.preventDefault()        onClick()      }}    >      {children}    </a>  )}Link.propTypes = {  active: PropTypes.bool.isRequired,  children: PropTypes.node.isRequired,  onClick: PropTypes.func.isRequired}export default Link

components/Footer.js

123456789101112131415161718192021222324
importimport  ReactReact  fromfrom  ''reactreact'' importimport  FilterLinkFilterLink  fromfrom  ''../containers/FilterLink../containers/Filter 'import { VisibilityFilters } from '../actions'const Footer = () => (  <p>    Show:    {' '}    <FilterLink filter={VisibilityFilters.SHOW_ALL}>      All    </FilterLink>    {', '}    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>      Active    </FilterLink>    {', '}    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>      Completed    </FilterLink>  </p>)export default Footer

实现容器组件

现在是时候通过创建一些容器将这些表示性组件连接到Redux。从技术上讲,容器组件只是一个React组件,它使用store.subscribe()来读取Redux状态树的一部分,并为它呈现的呈现组件提供props。您可以手动编写容器组件,但我们建议使用React Redux库的connect()函数生成容器组件,该函数提供许多有用的优化以防止不必要的重新呈现。(这样做的一个结果是你不必担心自己实现shouldComponentUpdate的React性能建议。)
要使用connect(),需要定义一个名为mapStateToProps的特殊函数,它告诉如何将当前的Redux存储状态转换为要传递给要包装的表示组件的道具。例如,VisibleTodoList需要计算待办事项以传递给TodoList,所以我们定义一个函数,根据state.visibilityFilter过滤state.todos,并在其mapStateToProps中使用它:

1234567891011121314151617
const getVisibleTodos = (todos, filter) => {  switch (filter) {    case 'SHOW_COMPLETED':      return todos.filter(t => t.completed)    case 'SHOW_ACTIVE':      return todos.filter(t => !t.completed)    case 'SHOW_ALL':    default:      return todos  }}const mapStateToProps = state => {  return {    todos: getVisibleTodos(state.todos, state.visibilityFilter)  }}

除了读取状态之外,容器组件还可以调度操作。以类似的方式,您可以定义一个名为mapDispatchToProps()的函数,它接收dispatch()方法并返回要注入演示组件的回调支持。例如,我们希望VisibleTodoList将名为onTodoClick的道具注入TodoList组件,并且我们希望onTodoClick发送TOGGLE_TODO操作:

1234567
const mapDispatchToProps = dispatch => {  return {    onTodoClick: id => {      dispatch(toggleTodo(id))    }  }}

最后,我们通过调用connect()并传递这两个函数来创建VisibleTodoList:

12345678
import { connect } from 'react-redux'const VisibleTodoList = connect(  mapStateToProps,  mapDispatchToProps)(TodoList)export default VisibleTodoList

这些是React Redux API的基础知识,但有几个捷径和开关选项,因此我们鼓励您详细查看其文档。如果您担心mapStateToProps过于频繁地创建新对象,则可能需要了解使用重新选择计算派生数据。
查找下面定义的其余容器组件:
container/FilterLink.js

123456789101112131415161718192021222324
import { connect } from 'react-redux'import { setVisibilityFilter } from '../actions'import Link from '../components/Link'const mapStateToProps = (state, ownProps) => {  return {    active: ownProps.filter === state.visibilityFilter  }}const mapDispatchToProps = (dispatch, ownProps) => {  return {    onClick: () => {      dispatch(setVisibilityFilter(ownProps.filter))    }  }}const FilterLink = connect(  mapStateToProps,  mapDispatchToProps)(Link)export default FilterLink

containers/VisibleTodoList.js

1234567891011121314151617181920212223242526272829303132333435
import { connect } from 'react-redux'import { toggleTodo } from '../actions'import TodoList from '../components/TodoList'const getVisibleTodos = (todos, filter) => {  switch (filter) {    case 'SHOW_ALL':      return todos    case 'SHOW_COMPLETED':      return todos.filter(t => t.completed)    case 'SHOW_ACTIVE':      return todos.filter(t => !t.completed)  }}const mapStateToProps = state => {  return {    todos: getVisibleTodos(state.todos, state.visibilityFilter)  }}const mapDispatchToProps = dispatch => {  return {    onTodoClick: id => {      dispatch(toggleTodo(id))    }  }}const VisibleTodoList = connect(  mapStateToProps,  mapDispatchToProps)(TodoList)export default VisibleTodoList

实现其他组件

containers/AddTodo.js
如前所述,AddTodo组件的展示和逻辑都被混合到一个单一的定义中。

12345678910111213141516171819202122232425262728293031323334
import React from 'react'import { connect } from 'react-redux'import { addTodo } from '../actions'let AddTodo = ({ dispatch }) => {  let input  return (    <div>      <form        onSubmit={e => {          e.preventDefault()          if (!input.value.trim()) {            return          }          dispatch(addTodo(input.value))          input.value = ''        }}      >        <input          ref={node => {            input = node          }}        />        <button type="submit">          Add Todo        </button>      </form>    </div>  )}AddTodo = connect()(AddTodo)export default AddTodo

如果您不熟悉ref属性,请阅读本文档以熟悉推荐使用此属性。

将容器捆绑在一个组件内

components/App.js

1234567891011121314
import React from 'react'import Footer from './Footer'import AddTodo from '../containers/AddTodo'import VisibleTodoList from '../containers/VisibleTodoList'const App = () => (  <div>    <AddTodo />    <VisibleTodoList />    <Footer />  </div>)export default App

传递Store

所有容器组件都需要访问Redux存储,以便他们可以订阅它。一种选择是将其作为props传递给每个容器组件。然而它很乏味,因为即使是通过表示组件,也只是因为它们碰巧在组件树中渲染容器,所以你必须连线存储。
我们推荐的选项是使用名为的特殊React Redux组件来神奇地使存储可供应用程序中的所有容器组件使用,而无需明确传递它。渲染根组件时,只需要使用它一次:
index.js

123456789101112131415
import React from 'react'import { render } from 'react-dom'import { Provider } from 'react-redux'import { createStore } from 'redux'import todoApp from './reducers'import App from './components/App'const store = createStore(todoApp)render(  <Provider store={store}>    <App />  </Provider>,  document.getElementById('root'))

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!