前面的两篇文章咱们认识了 Redux 的相关知识以及解决了如何使用异步的action,基础知识已经介绍完毕,接下来,咱们就能够在React中使用Redux了。 javascript
因为Redux只是一个状态管理工具,不针对任何框架,因此直接使用Redux作React项目是比较麻烦的,为了方便Redux结合React使用,Redux的做者建立了React-Redux, 这样,咱们就能够经过React-Redux将React和Redux连接起来了,固然Redux仍是须要的,React-Redux只是基于Redux的,因此在通常项目中,咱们须要使用 Redux 以及 React-Redux二者。html
虽然安装React-Redux须要掌握额外的API,可是为了方便咱们对状态的管理,仍是最好使用React-Redux。java
可参考的官方文档1:http://cn.redux.js.org/docs/basics/UsageWithReact.htmlnode
可参考的官方文档2:http://cn.redux.js.org/docs/basics/ExampleTodoList.htmlreact
推荐文章: https://github.com/bailicangdu/blog/issues/3
git
React-Redux 将全部组件分红两大类:UI 组件(presentational component)和容器组件(container component)。es6
UI 组件有如下几个特征:github
只负责 UI 的呈现,不带有任何业务逻辑 没有状态(即不使用this.state这个变量) 全部数据都由参数(this.props)提供 (对于这样的组件咱们使用function的建立方式便可) 不使用任何 Redux 的 API (由于UI组件仅仅是为了展现,而没有数据的掺杂)
下面就是一个 UI 组件的例子:(这里用的就是function的方式建立的组件,箭头函数语法)redux
const Title = value => <h1>{value}</h1>;
由于不含有状态,UI 组件又称为"纯组件",即它纯函数同样,纯粹由参数决定它的值。app
容器组件的特征偏偏相反:
负责管理数据和业务逻辑,不负责 UI 的呈现
带有内部状态
使用 Redux 的 API
总之,只要记住一句话就能够了:UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。
React-Redux 规定,全部的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是所有交给它。能够看出React-Redux仍是很是有用的。
组件类型补充,以前提到了UI组件和容器组件,实际上,咱们组件的分类还有
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
方法接受两个参数:mapStateToProps
和mapDispatchToProps
。它们定义了 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。
咱们有意模糊了定义中“包裹”的概念,由于它可能会有如下两种不一样的含义之一:
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
你能够读取、添加、编辑、删除传给 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 (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
是一个函数。它的做用就是像它的名字那样,创建一个从(外部的)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
是connect
函数的第二个参数,用来创建 UI 组件的参数到store.dispatch
方法的映射。也就是说,它定义了哪些用户的操做应该看成 Action,传给 Store。它能够是一个函数,也能够是一个对象。
若是mapDispatchToProps
是一个函数,会获得dispatch
和ownProps
(容器组件的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中的值过滤出来。
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 组件有两个参数:value
和onIncreaseClick
。前者须要从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的项目,与其余项目没有不一样之处,也是使用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://stackoverflow.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') )
在安装了 redux 和 react-redux 以后,咱们能够在 node_modules 中看到 prop-type 模块,而后在上面的例子中咱们也引入了这个模块,那么这个模块的做用是什么呢?
https://stackoverflow.com/questions/40228481/proptypes-in-react-redux
在上面的连接中,stackoverflow 给出了很好的解释, 即:
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是不是必须的、而且能够规订传入的类型是否有问题,这样均可以方便咱们检查这个模块存在的问题。
在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://stackoverflow.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 }); } }; }