内容


使用 Redux 管理状态,第 2 部分

结合使用 Redux 和 React

应用 react-redux 绑定;实现操作创建器

系列内容:

此内容是该系列 5 部分中的第 # 部分: 使用 Redux 管理状态,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 Redux 管理状态,第 2 部分

敬请期待该系列的后续内容。

使用 Redux 管理状态 系列的 第 1 部分 中,学习了 Redux 的基础知识,了解了如何单独使用 Redux,并开始探索如何结合使用 Redux 和 React 框架。本期文章将继续该探索,首先讨论 Redux 的 React 绑定,该绑定使得将无状态表示组件与连接到 React 的组件分离成为可能。然后我将转而介绍一个更复杂的应用程序(一个也使用 React 的应用程序),用该应用程序来演示 Redux 的更高级方面。

使用 react-redux 绑定

GitHub 上提供了针对多个流行框架的 Redux 绑定,包括 React、Angular 和 Vue。这些绑定使得将 Redux 与通过这些框架构建的应用程序相集成变得很容易。

react-redux 绑定提供了一个 API,该 API 包含一个 React 组件和一个 JavaScript 方法:

  • Provider 组件允许 Provider 组件中包含的组件访问 Redux 存储。
  • void connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) 函数将一个 React 表示组件连接到 Redux 存储。

Provider 组件很方便,您不需要在整个组件分层结构中手动传递存储作为一个属性。嵌套在 Provider 组件内的组件会自动获得 Redux 存储的访问权。

connect() 函数将一个表示组件连接到 Redux 存储,以便在存储更改时更新该组件。

与 Redux 存储的自动连接

第 1 部分 中,了解了如何将单个 React 组件连接到 Redux 存储。这种风格非常流行,以至于 redux-react 绑定提供了一个 connect() 方法来自动将无状态功能组件连接到存储。

为了演示,我首先将修改 第 1 部分 的交通信号灯应用程序的 App 组件,让其使用称为容器组件 的组件 — 这类组件会自动连接到 Redux 存储,并向一个封闭的无状态功能组件提供属性。清单 1 给出了更新后的 App 组件。

清单 1. 应用程序 (app.js),其中现在使用了容器组件
import React, { Component } from 'react';
import { StoplightContainer } from './stoplight-container';
import { ButtonContainer } from './button-container';
import { createStore } from 'redux';
import { reducer } from './reducer';

export class App extends Component {
  render() {
    const store = createStore(reducer);

    return(
      <div>
        <StoplightContainer store={store}/>
        <ButtonContainer store={store}/>
      </div>
    )
  }
}

没有像原始实现中一样返回包含 StoplightButtons 组件的 DIVApp 组件的经过改良的 render() 方法返回了一个包含两个容器组件的 DIVStoplightContainerButtonContainer

请注意,我仍将 Redux 存储传递给了 App 组件中包含的组件。您会在下一节看到如何规避该需求。

清单 2 显示了 stoplight 容器。

清单 2. stoplight 容器 (stoplight-container.js)
import { connect } from 'react-redux';
import { Stoplight } from './stoplight';

const mapStateToProps = state => {
  return {
    goColor:      state == 'GO'      ? 'rgb(39,232,51)' : 'white',
    cautionColor: state == 'CAUTION' ? 'yellow' : 'white',
    stopColor:    state == 'STOP'    ? 'red' : 'white'
  }
}

const mapDispatchToProps = null;

export const StoplightContainer = connect(
    mapStateToProps,
    mapDispatchToProps
)(Stoplight);

简单来讲,StoplightContainer 连接到 Redux 存储,并将应用程序状态映射到它包含的 stoplight 的属性。应用程序状态为 GOCAUTIONSTOP,stoplight 的属性为 goColorcautionColorstopColor

默认情况下,stoplight 的 3 个属性中的每一个都是 white。当状态为 GO 时,stoplight 容器会将 stoplight 的 goColor 属性映射到绿灯(编码为 rgb(39,232,51))。当状态为 CAUTION 时,会将 cautionColor 映射到 yellow。当状态为 STOP,会将 stopColor 映射到 red

为了将应用程序状态映射到 stoplight 属性,清单 2 使用了 react-redux 绑定的 connect() 函数将 StoplightContainer 连接到 Redux 存储。

通过调用 connect() 函数并将 Stoplight 传递给 connect() 返回的函数,Redux 在 Redux 存储中的状态发生更改时自动更新 Stoplight。您只需调用 connect() 即可实现此目的。当存储发生更改时,Redux 会调用 StoplightContainermapStateToProps() 方法。Redux 将 StoplightContainer.mapStateToProps() 返回的对象的属性值复制到 StoplightContainer 中包含的 stoplight

connect() 方法接受两个参数,这两个参数都是函数。第一个函数将来自 Redux 存储的状态映射到所包含的组件(在本例中为 Stoplight)的属性。第二个函数将 Redux 分派调用映射到属性;但是,stoplight 不启用任何行为,所以 stoplight 容器不会将分派调用映射到属性。结果,stoplight 容器的 mapDispatchToProps 函数为 null

清单 3 显示了 Stoplight 组件的经过再次改良的实现,其中使用了它的 3 个属性作为 SVG 圆圈的 fill 属性。

清单 3. Stoplight (stoplight.js),已恢复为无状态功能组件
import React, { Component } from 'react';

export const Stoplight = ({
  goColor,
  cautionColor,
  stopColor
}) => {
  return(
    <div style={{textAlign: 'center'}}>
      <svg height='170'>
        <circle cx='145' cy='60' r='15'
                fill={stopColor}
                stroke='black'/>

        <circle cx='145' cy='100' r='15'
                fill={cautionColor}
                stroke='black'/>

        <circle cx='145' cy='140' r='15'
                fill={goColor}
                stroke='black'/>
      </svg>
    </div>
  )
}

Stoplight 组件已恢复为无状态功能组件,它从 StoplightContainer 组件接收自己的属性。

清单 4 显示了 ButtonContainer 组件。

清单 4. 按钮容器 (button-container.js)
import { connect } from 'react-redux';
import { Buttons } from './buttons';
import { goAction, cautionAction, stopAction } from './actions';

const mapStateToProps = state => {
  return {
    lightStatus: state
  }
}

const mapDispatchToProps = dispatch => {
  return {
    go:      () => { dispatch(goAction) },
    caution: () => { dispatch(cautionAction) },
    stop:    () => { dispatch(stopAction) }
  }
}

export const ButtonContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Buttons);

ButtonContainer 组件将当前状态映射到一个名为 lightStatusButtons 属性。信号灯状态就是状态的值(GOCAUTIONSTOP)。

不同于 Stoplight 组件,Buttons 组件中的按钮会发起改变状态的行为,所以 ButtonContainerdispatch() 调用映射到 Buttons 属性。这些属性是 Buttons 组件使用的函数,如清单 5 所示。

清单 5. Buttons 组件 (buttons.js),也已恢复为无状态功能组件
import React, { Component } from 'react';

export const Buttons = ({
  go,
  caution,
  stop,
  lightStatus
}) => {
  return(
    <div style={{textAlign: 'center'}}>
      <button onClick={go}
              disabled={lightStatus == 'GO' || lightStatus == 'CAUTION'}
              style={{cursor: 'pointer'}}>
        Go
      </button>

      <button onClick={caution}
              disabled={lightStatus == 'CAUTION' || lightStatus == 'STOP'}
              style={{cursor: 'pointer'}}>
        Caution
      </button>

      <button onClick={stop}
              disabled={lightStatus == 'STOP' || lightStatus == 'GO'}
              style={{cursor: 'pointer'}}>
        Stop
      </button>
    </div>
  )
}

清单 5 中的 Buttons 组件使用它的 gocautionstop 属性(都是函数)作为每个按钮的 onClick 处理函数的回调。这些属性来自 ButtonContainer 组件。请注意,像 Stoplight 组件一样,Buttons 组件已恢复为无状态功能组件。

表示组件与容器组件的分离

react-redux 绑定不仅提供了与 Redux 存储的自动连接,还通过将关注点分离到容和关联的无状态组件来帮助执行良好的编程实践。容器组件实现 mapStateToProps()(用于将状态映射到数据)和 mapDispatchToProps()(用于将状态映射到行为)。这种分离有诸多好处:

  • 表示组件很容易实现和推断。
  • 表示组件很容易测试,因为它们不会改变数据。
  • 表示组件可在不同数据源中重用。
  • 容器组件很容易测试,因为它们没有表示代码。

react-redux 绑定的 connect() 函数已介绍得足够多了;现在让我们来看看 React Provider 组件。

Redux 提供程序

使用 Redux 的 connect() 函数的一个良好的辅助影响是,StoplightButtons 等无状态功能组件不再需要直接访问 Redux 存储 — 因为这些组件不再根据状态来计算它们的属性。相反,相应的容器组件将 Redux 存储与应用程序的无状态功能组件联系起来。这种安排使得无状态功能组件(比如 StoplightButton 的最终版本)更容易测试。

但是,容器组件仍访问应用程序状态,将它映射到容器包含的无状态组件的属性。要使 Redux 存储可用于应用程序的 React 组件,可以在组件分层结构中显式向下传递它,或者可以使用 Provider,如清单 6 所示。

清单 6. 使用 Provider 组件 (index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import Redux, { createStore } from 'redux';
import { Provider } from 'react-redux';

import { reducer } from './reducer';
import { App } from './app';

ReactDOM.render(
  <Provider store={createStore(reducer)}>
    <App />
  </Provider>,
  document.getElementById('root')
)

您为 Provider 指定的属性自动可用于 Provider 组件中包含的任何 React 组件。在本例中,App 组件(以及 App 组件中包含的 StoplightContainerButtonContainer 组件)会自动获取 Redux 存储的访问权。

目前您已通过一个简单应用程序了解了 Redux 基础原理和 react-redux 绑定,该应用程序具有最简单的状态 — 单个字符串。要理解 Redux 的更高级方面,是时候查看一个更复杂的应用程序了。

图书搜索应用程序

您已深入掌握 Redux,但仍有许多基础知识需要介绍,其中包括:

  • 实现和使用操作创建器
  • 组合缩减程序
  • 创建异步操作
  • 实现撤销和重做
  • 实现状态时间线

我将使用图 1 中所示的应用程序来演示前面提到的这些主题,本期文章将介绍第一个主题,剩余主题以后再介绍。图书搜索应用程序使用 Google Books REST API 异步搜索图书。用户在文本字段中输入一个主题并按 Enter 键后,应用程序抓取前 10 部与该主题匹配的图书的信息,显示每部图书的封面缩略图。

图 1. 图书搜索应用程序
在图书搜索应用程序中搜索结果时的屏幕截图
在图书搜索应用程序中搜索结果时的屏幕截图

图书缩略图是链接。单击缩略图,就会在 books.google.com 上看到该图书的更多信息,如图 2 所示。

图 2. 单击一个图书缩略图的结果
图书细节页面的屏幕截图
图书细节页面的屏幕截图

该应用程序提供了一种替代性的列表视图,如图 3 所示。可单击 List 单选按钮来激活列表视图。

图 3. 列表视图
book-detail 列表视图的屏幕截图
book-detail 列表视图的屏幕截图

最后,该应用程序支持撤销(通过单击向左箭头)和重做(通过单击向右箭头),以及一个可在应用程序的以前状态中前后移动的历史滑块。

以上是图书搜索应用程序的概述。接下来,我将介绍如何使用 React 和 Redux 实现该应用程序。

组件

首先,我将创建该应用程序的一个原型,如图 4 所示。

图 4. 原型
应用程序原型图
应用程序原型图

接下来,我将设计一种组件分层结构,如图 5 所示。

图 5. 图书搜索应用程序的组件
组件原型
组件原型

组件分层结构类似于:

  • 应用程序
    • 控件
      • 主题选择器
      • 显示选项
      • 历史
    • 图书
      • 图书
      • ...
    • 状态查看器

这是该应用程序的目录结构和关联的文件:

actions.js
  components
    book.css
    book.js
    books.js
    controls.js
    displayModeOptions.js
    history.js
    stateviewer.js
    topicselector.js
  containers
    app.js
    books.js
    controls.js
    history.js
    stateviewr.js
    topicselector.js
images
  app.js
index.html
index.js
middleware.js
node_modules
package.json
reducers.js
statehistory.js
store.js
webpack.config.js

该应用程序的 8 个组件中的 7 个的具有相应的 Redux 容器组件,总共有 15 个组件。容器组件位于容器目录中,表示组件位于组件目录中。

应用程序入口点

图 6 显示了图书搜索应用程序的起点,其中包含具有主题选择器和显示选项的控件组件。

图 6. 图书搜索应用程序的起点
图书搜索应用程序的开始页面的屏幕截图
图书搜索应用程序的开始页面的屏幕截图

清单 7 给出了入口点的代码,该代码呈现 App 组件。该组件包装在一个 Redux Provider 组件内。回想一下,提供程序使 Redux 存储可用于该应用程序。

清单 7. 入口点 (index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import { App } from './containers/app';
import { store } from './store';
import { setTopic, setDisplayMode } from './actions';

store.dispatch(setTopic('javascript'));
store.dispatch(setDisplayMode('THUMBNAIL'));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('example')
)

function showState() {
  const state = store.getState();
  debugger
}

store.subscribe(showState);

该应用程序的入口点也订阅了 Redux 存储;当存储中的状态发生更改时,Redux 调用 showState(),后者获取应用程序的状态并调用调试程序。

在呈现应用程序并订阅 Redux 存储之前,入口点分派了两个操作,一个用于设置图书主题,另一个用于设置显示模式。

不同于 第 1 部分 中的 stoplight 示例中对 store.dispatch() 的调用,清单 7 中的两次分派调用使用函数(setTopic()setDisplayMode())来创建操作对象。这些函数称为操作创建器

实现操作创建器

清单 8 给出了 set(Topic)setDisplayMode() 操作创建器的实现 — 这两个函数接受一个参数并返回一个相应的操作。

清单 8. 操作 (actions.js)
export const setTopic = topic => {
  return {
    type: 'SET_TOPIC',
    topic
  }
}

export const setDisplayMode = displayMode => {
  return {
    type: 'SET_DISPLAY_MODE',
    displayMode
  }
}

操作创建器看起来像一种设计操作对象的迂回方式。直接指定操作更简单。但是,操作创建器通常在一个或少量文件中实现,这使得查找应用程序操作的代码变得很容易,这些文件实际上相当于某种形式的文档。

结束语

在本期文章中,您了解了如何使用 react-redux 绑定自动连接到 Redux 存储,并将容器组件与其对应的表示组件分离。容器组件连接到 Redux 存储,当存储的状态发生更改时,容器组件将当前状态映射到表示组件的属性。这些属性可以是数据或函数。

您还看到,没有必要在整个组件分层结构中传递 Redux 存储。可以使用 react-redux 绑定所带来的 Provider 组件,使 Redux 存储可用于所有组件。

最后,您已开始通过图书搜索示例了解高级 Redux 特性。您已了解到,可以将操作的创建封装到函数中,使代码更容易理解。

下一期文章 将继续通过图书搜索示例应用程序介绍高级 Redux 特性。您将了解如何通过组合缩减程序来处理更复杂的状态,并开始学习如何实现异步操作。


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=1038374
ArticleTitle=使用 Redux 管理状态,第 2 部分: 结合使用 Redux 和 React
publish-date=10122016