前面的兩篇文章咱們認識了 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 }); } }; }