结合React使用Redux

  前面的两篇文章我们认识了 Redux 的相关知识以及解决了如何使用异步的action,基础知识已经介绍完毕,接下来,我们就可以在React中使用Redux了。 

  由于Redux只是一个状态管理工具,不针对任何框架,所以直接使用Redux做React项目是比较麻烦的,为了方便Redux结合React使用,Redux的作者创建了React-Redux, 这样,我们就可以通过React-Redux将React和Redux链接起来了,当然Redux还是需要的,React-Redux只是基于Redux的,所以在一般项目中,我们需要使用 Redux 以及 React-Redux两者。

  虽然安装React-Redux需要掌握额外的API,但是为了方便我们对状态的管理,还是最好使用React-Redux。

  可参考的官方文档1:http://cn.redux.js.org/docs/basics/UsageWithReact.html

  可参考的官方文档2:http://cn.redux.js.org/docs/basics/ExampleTodoList.html

  推荐文章: https://github.com/bailicangdu/blog/issues/3

一、UI组件

  React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。

       UI 组件有以下几个特征:

只负责 UI 的呈现,不带有任何业务逻辑
没有状态(即不使用this.state这个变量所有数据都由参数(this.props)提供 (对于这样的组件我们使用function的创建方式即可)
不使用任何 Redux 的 API (因为UI组件仅仅是为了展示,而没有数据的掺杂)

  下面就是一个 UI 组件的例子:(这里用的就是function的方式创建的组件,箭头函数语法)

const Title =
  value => <h1>{value}</h1>;

  因为不含有状态,UI 组件又称为"纯组件",即它纯函数一样,纯粹由参数决定它的值

二、 容器组件

  容器组件的特征恰恰相反:

负责管理数据和业务逻辑,不负责 UI 的呈现
带有内部状态
使用 Redux 的 API

  总之,只要记住一句话就可以了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。 

  你可能会问,如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

  React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。可以看出React-Redux还是非常有用的。


组件类型补充,之前提到了UI组件和容器组件,实际上,我们组件的分类还有

  • 交互型组件。比如创建一个展示列表,用户可以点击,然后这个组件给与一定的回馈。
  • 功能性组件。即实现某些特定功能,如<router-view>组件和<transition>组件。

三、 connect()

  React-Redux 提供connect方法,用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。

import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

  上面代码中,TodoList是 UI 组件,VisibleTodoList就是由 React-Redux 通过connect方法自动生成的容器组件

  但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息:

1)输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数
(2)输出逻辑:用户发出的动作如何变为 Action 对象,从 UI 组件传出去

  即使用Redux的作用就是管理state,如果没有state的输入输出,那么我们就不必使用redux来管理状态,这样,容器组件的包装就没有必要了

  

  因此,connect方法的完整 API 如下:

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

  即我们给这个容器对象传入了mapStateToProps以及mapDispatchToProps。 

  上面代码中,connect方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。mapStateToProps 负责输入逻辑,即将state映射到 UI 组件的参数(props),mapDispatchToProps负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

  





实际上: 这里的connect()函数是一个高阶组件

高阶组件介绍:

什么是高阶组件

  高阶组件就是HOC(Higher Order Component)--- 高阶组件是一个React组件包裹着另外一个React组件。

  这种模式通常使用函数来实现,如下(haskell):

hocFactory:: W: React.Component => E: React.Component

  其中W(wrappedComponent)是指被包裹的React.Component, E(EnhancedComponent)值得是返回类型为React.Component的新的HOC。 

  

我们有意模糊了定义中“包裹”的概念,因为它可能会有以下两种不同的含义之一:

  1. Props Proxy: HOC 对传给 WrappedComponent W 的 porps 进行操作,
  2. Inheritance Inversion: HOC 继承 WrappedComponent W。

Props Proxy

Props Proxy 的最单的实现

function ppHOC(WrappedComponent) {  
  return class PP extends React.Component {    
    render() {      
      return <WrappedComponent {...this.props}/>    
    }  
  } 
}

这里主要是 HOC 在 render 方法中 返回 了一个 WrappedComponent 类型的 React Element。我们还传入了 HOC 接收到的 props,这就是名字 Props Proxy 的由来。

使用 Props Proxy 可以做什么?

  • 操作 props
  • 通过 Refs 访问到组件实例
  • 提取 state
  • 用其他元素包裹 WrappedComponent

 操作 props

你可以读取、添加、编辑、删除传给 WrappedComponent 的 props。

当删除或者编辑重要的 props 时要小心,你可能应该通过命名空间确保高阶组件的 props 不会破坏 WrappedComponent

例子:添加新的 props。在这个应用中,当前登录的用户可以在 WrappedComponent 中通过 this.props.user 访问到。

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

通过 Refs 访问到组件

为什么要用高阶组件?

你可以通过引用ref)访问到 this (WrappedComponent 的实例),但为了得到引用,WrappedComponent 还需要一个初始渲染,意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。

例子:如何通过 refs 访问到实例的方法和实例本身:

function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/>
    }
  }
}

 提取 state

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

Inheritance Inversion

Inheritance Inversion (II) 的最简实现:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

你可以看到,返回的 HOC 类(Enhancer)继承了 WrappedComponent。之所以被称为 Inheritance Inversion 是因为 WrappedComponent 被 Enhancer 继承了,而不是 WrappedComponent 继承了 Enhancer。在这种方式中,它们的关系看上去被反转(inverse)了。

Inheritance Inversion 允许 HOC 通过 this 访问到 WrappedComponent,意味着它可以访问到 state、props、组件生命周期方法和 render 方法。

命名

用 HOC 包裹了一个组件会使它失去原本 WrappedComponent 的名字,可能会影响开发和调试。

通常会用 WrappedComponent 的名字加上一些 前缀作为 HOC 的名字。下面的代码来自 React-Redux:

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`

//

class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
  ...
}

案例分析

  react-redux: 是redux官方的react绑定实现,它提供了一个connect函数,这个函数处理了监听store和后续的处理,就是通过props proxy来实现的。 

四、 mapStateToProps()

  

  mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子:
  

const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

上面代码中,mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表 UI 组件的同名参数,后面的getVisibleTodos也是一个函数,可以从state算出 todos 的值。

下面就是getVisibleTodos的一个例子,用来算出todos

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)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。

mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。

// 容器组件的代码
//    <FilterLink filter="SHOW_ALL">
//      All
//    </FilterLink>

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发 UI 组件重新渲染。

connect方法可以省略mapStateToProps参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

实际上我们可以看出使用mapStateToProps实际上就是从 store 中取出我们想要的数据。

五、 mapDispatchToProps()

  mapDispatchToPropsconnect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。

  如果mapDispatchToProps是一个函数,会得到dispatchownProps(容器组件的props对象)两个参数。

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

  OK! 这里就是重点了,通过mapDispatchToProps我们就可以在改变view层的时候通过dispath(action)使得store中的数据发生变化。这样就和我们在介绍Redux的基本概念时相一致了。 

  从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。

如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。

const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

  

不难看出,我们是可以自己定义mapStateToProps函数以及 mapDispatchToProps函数的,第一个函数的作用是为了将 store 中的 state 注入到组件中,即通过在 return 上面使用 const {} = this.props 的形式,因为通过 mapStateToProps 以及 <Provider> 的使用,我们就可以先将state传入到组件中,然后通过mapStateToProps将我们想要的state中的值过滤出来。

六、 <Provider>组件

  connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。

  一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。

  React-Redux 提供Provider组件,可以让容器组件拿到state。 

  即<Provider>组件的作用就是为了将state更加方便地传递给容器组件。 

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

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

  上面代码中,Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。

  它的原理是React组件的context属性,请看源码。

  

class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }
  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: React.PropTypes.object
}

  上面代码中,store放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。

  

class VisibleTodoList extends Component {
  componentDidMount() {
    const { store } = this.context;
    this.unsubscribe = store.subscribe(() =>
      this.forceUpdate()
    );
  }

  render() {
    const props = this.props;
    const { store } = this.context;
    const state = store.getState();
    // ...
  }
}

VisibleTodoList.contextTypes = {
  store: React.PropTypes.object
}

  

  React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store。

七、实例 --- 计数器

  我们来看一个实例。下面是一个计数器组件,它是一个纯的 UI 组件。

  

class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>Increase</button>
      </div>
    )
  }
}

  上面代码中,这个 UI 组件有两个参数:valueonIncreaseClick。前者需要从state计算得到,后者需要向外发出 Action。

 

  接着,定义value到state的映射,以及onIncreaseClick到dispatch的映射。

function mapStateToProps(state) {
  return {
    value: state.count
  }
}

function mapDispatchToProps(dispatch) {
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}

// Action Creator
const increaseAction = { type: 'increase' }

  

  然后,使用connect方法生成容器组件。

const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

  然后,定义这个组件的 Reducer。  

function counter(state = { count: 0 }, action) {
  const count = state.count
  switch (action.type) {
    case 'increase':
      return { count: count + 1 }
    default:
      return state
  }
}

  

  最后,生成store对象,并使用Provider在根组件外面包一层。  

  

  

import { loadState, saveState } from './localStorage';

const persistedState = loadState();
const store = createStore(
  todoApp,
  persistedState
);

store.subscribe(throttle(() => {
  saveState({
    todos: store.getState().todos,
  })
}, 1000))

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

八、 React-Router路由库

  使用React-Router的项目,与其他项目没有不同之处,也是使用Provider在Router外面包一层,毕竟Provider的唯一功能就是传入store对象

  如下所示:

const Root = ({ store }) => (
  <Provider store={store}>
    <Router>
      <Route path="/" component={App} />
    </Router>
  </Provider>
);

  

  

  

九 例子

下面是一个完整的例子:// 引入React,写组件时使用import React, { Component } from 'react'

// 引入 prop-types, 即属性类型模块
import PropTypes from 'prop-types'

// react-dom核心代码 import ReactDOM
from 'react-dom'

// 用于创建redux中的store
import { createStore } from 'redux'

// 使用Provider将state传入组件内部,使用connet将UI组件添加一层业务逻辑容器形成容器组件然后导出
import { Provider, connect } from 'react-redux' // 创建 React 组件 Couter class Counter extends Component { render() {
   // 通过es6的解构赋值拿到 props 中的value值和onIncreaseClick
const { value, onIncreaseClick } = this.props return ( <div> <span>{value}</span> <button onClick={onIncreaseClick}>Increase</button> </div> ) } }
// 从prop-types中引入的 PropTypes 是什么? 我们可以在 https://*.com/questions/40228481/proptypes-in-react-redux 这个问题上找到答案。即确定这个组件的类型是否正确 Counter.propTypes
= {
 // value要求必须是 number 类型。 value: PropTypes.number.isRequired,

 // onIncreaseClick 要求必须是 function 类型。 onIncreaseClick: PropTypes.func.isRequired }
// Action
// 定义一个ACTION,在点击的时候会触发这个action
const increaseAction = { type: 'increase' } // Reducer
// 创建一个reducer,这样就可以告诉store对象如何处理通过click发送过去的action了。
function counter(state = { count: 0 }, action) { const count = state.count switch (action.type) { case 'increase': return { count: count + 1 } default: return state } } // Store
// 基于reducer创建一个 store 仓库
const store = createStore(counter)

// Map Redux state to component props function mapStateToProps(state) { return { value: state.count } } // Map Redux actions to component props function mapDispatchToProps(dispatch) { return { onIncreaseClick: () => dispatch(increaseAction) } } // Connected Component const App = connect( mapStateToProps, mapDispatchToProps )(Counter) ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )

10、 propTypes 是什么? 

  在安装了 redux 和 react-redux 之后,我们可以在 node_modules 中看到 prop-type 模块,然后在上面的例子中我们也引入了这个模块,那么这个模块的作用是什么呢

  https://*.com/questions/40228481/proptypes-in-react-redux 

  在上面的链接中,* 给出了很好的解释, 即:

How we use propTypes at our work is to have a better understanding of each component right from the get go. You can see the shape of the component based off the props you pass in and get a better idea of how it works.

Its also great with the .isRequired because you will get warnings if it wasn't included when the component was created. It will also give warnings if a prop was expected to be one type but was actually passed down as something different.

It is by no means necessary but it will make developing alongside others much easier since you can learn about what the component expects to be passed down and in what form. This becomes much more critical when there are new components being created almost daily and you are trying to use a component made by someone else and have never touched it before.

  即通过这个模块,我们可以规定其所需要的的props是否是必须的、并且可以规订传入的类型是否有问题,这样都可以方便我们检查这个模块存在的问题。

11、 

  在9的例子中,我们发现下面的函数: 

function mapStateToProps(state) {
  return {
    value: state.count
  }
}

  

  这个函数式是定义在下面的之前的:

// Connected Component 
const App = connect(
  mapStateToProps,
  mapDispatchToProps
)(Counter)

  

  不难看出,我们应该是可以修改mapStateToProps的名字的,但是最好不要这样。 

  问题: 为什么 function mapStateToProps 提前定义,却可以接收到 state 值呢?

The React-Redux connect function generates a wrapper component that subscribes to the store. That wrapper component calls store.getState() after each dispatched action, calls the supplied mapStateToProps function with the current store state, and if necessary, calls mapDispatchToProps with the store's dispatch function.

Dan wrote a simplified version of connect a while back to illustrate the general approach it uses. See https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e .

  在 https://*.com/questions/39045378/how-is-redux-passing-state-to-mapstatetoprops 这篇文章中,我们可以看到 connect 函数实际上是对UI组件的一个封装,这个封装订阅了store对象,并且在其中调用了 store.getState() 函数,这样就可以得到state值了,然后把之前定义的带有state参数的函数出入进去,这个state参数就会自动获得 connect 函数中产生的state值了。 对于mapDispatchToProps也是如此。

说明: 本文章多参考阮一峰老师的文章,由衷敬佩。

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}