前面的兩篇文章我們認識了 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
方法接受兩個參數: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: HOC 對傳給 WrappedComponent W 的 porps 進行操作,
- 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()
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中的值過濾出來。
六、 <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 組件有兩個參數: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路由庫
使用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') )
10、 propTypes 是什么?
在安裝了 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是否是必須的、並且可以規訂傳入的類型是否有問題,這樣都可以方便我們檢查這個模塊存在的問題。
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://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
});
}
};
}