當Redux 和React 相接合,就是使用Redux進行狀態管理,使用React 開發頁面UI。相比傳統的html, 使用React 開發頁面,確實帶來了很多好處,組件化,代碼復用,但是和Redux 接合時,組件化卻也帶來了一定的問題,組件層層嵌套,有成千上百個,而store確只有一個,組件中怎么才能獲取到store? 頁面UI就是顯示應用程序狀態的,如果獲取不到store中的state, 那就沒法渲染內容了。還有一個問題,就是如果狀態發生了變化,組件怎么做到實時監聽,實時顯示最新的狀態?
對於第一個問題,React組件中怎么獲取到store,你可能想到了, 在整個應用程序的最外層組件中把store 作為props 層層向下傳遞,對於一個小程序,還可以接受, 但對於一個大型程序呢,不可能成千上百個組件中都寫上store 屬性吧。還有一個解決方案就是context, 把所有組件包含在一個context中,context 提供store 屬性,這樣就不用層層傳遞,且所有的組件都會獲取到store.,方案可以一試
對於第二個問題,組件內部想要實時顯示最新的狀態,那就要使用store.subscribe() 方法,在其里面注冊監聽函數,獲取最新狀態,然后注入到組件中,組件更新的方法,就是調用setState() 方法,那我們的每一個組件都變成了有狀態的組件。那store.subsribe() 方法,什么時候注冊監聽函數,必須組件加載完就要注冊,componentDidMounted 里調用store.subscribe().
根據以上兩點分析,嘗試寫一下代碼,看不能能實現Redux和React 的接合,使用create-react-app 創建項目react-redux-demo,然后 cd react-redux-demo && npm i bootstrap redux --save,安裝boostrap 和redux。 打開項目,在index.js 中引入boostrap.
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import 'bootstrap/dist/css/bootstrap.css'; // 添加bootstrap 樣式 ReactDOM.render(<App />, document.getElementById('root'));
還是最簡單的加減counter 開始,點擊add 加1, 點擊minus 減1, 點擊reset 重置。看一下Redux, 由於Redux 就是action, reducer, store,和React 一點關系都沒有,所以完全把創建action,創建stroe的內容寫成單獨的文件,只暴露出React需要的東西給它調用就好了。React 需要store, 需要action, 因為它要dispatch action來改變狀態。簡單起見,把redux的有關內容都放到一個文件中,在src目錄下新建一個文件redux.js
import {createStore} from 'redux';
// state const initialState = { counter: 5 }; // action const add = { type: 'ADD' }; const minus = { type: 'MINUS' }; const reset = { type: 'RESET' }; // reducer function counter(state = initialState, action) { switch(action.type) { case 'ADD': return { ...state, counter: state.counter + 1 } case 'MINUS': return { ...state, counter: state.counter - 1 } case 'RESET': return { ...state, counter: 5 }; default: return state; } } // 創建store const store = createStore(counter); // export 出去store 和 action export {store, add, minus, reset};
現在就要寫React,創建頁面ui, 先不管交互,先把頁面三個按鈕和狀態的顯示畫出來,在src下創建一個ThreeButton.js,
import React, { Component } from 'react' export default class ThreeButton extends Component { render() { return ( <div style={{textAlign: "center"}}> <h1 id="counter">0</h1> <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}>Add</button> <button type="button" className="btn btn-success" style={{marginRight: '10px'}}>Minus</button> <button type="button" className="btn btn-danger">Reset</button> </div> ) } }
然后在App.js中引入
import React from 'react'; import ThreeButton from './ThreeButton'; function App() { return ( <ThreeButton></ThreeButton> ); } export default App;
准備實現React和Redux的接合,實現頁面的交互。首先就是要把store 注入到React中,使用React 的context api. context使用的最開始,是使用createContext創建一個context, 在src 目錄下新建一個storeContext.js
import React from 'react';
const storeContext = React.createContext({store: {}}) export {storeContext};
storeContext 有一個屬性Provider, 它是一個組件,有一個value屬性,提供真正的組件共享數據,這里就是Redux 創建的store 了。然后用Provider 把組件包起來,該組件和它的子組件都能夠獲取到共享數據,那就把App 包起來, 那就在index.js中把Redux的store和storeContext.js 引入
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import 'bootstrap/dist/css/bootstrap.css'; // 添加bootstrap 樣式 import { store } from './redux'; // 引入 store
// 提供sotre 作為共享數據,App及其子組件都能獲取到store const Provider = <storeContext.Provider value={{store: store}}> <App /> </storeContext.Provider> ReactDOM.render(Provider, document.getElementById('root'));
那ThreeButton.js 就可以獲取到store, 那具體是怎么獲取到store的呢?首先還是引入storeContext, 然后在類中加一個靜態屬性contextType, 它賦值為storeContext, 然后組件中就可以使用this.context 獲取到store 了。
import React, { Component } from 'react'; import { storeContext } from './storeContext'; // 引入storeContext export default class ThreeButton extends Component { static contextType = storeContext; // 加靜態屬性contextType, 賦值為storeContext componentDidMount() { let {store} = this.context; // this.context 獲取到store console.log(store); } render() { return ( <div style={{textAlign: "center"}}> <h1 id="counter">0</h1> <button type="button" className="btn btn-primary" style={{marginRight: '10px'}}>Add</button> <button type="button" className="btn btn-success" style={{marginRight: '10px'}}>Minus</button> <button type="button" className="btn btn-danger">Reset</button> </div> ) } }
可以看到控制台打印出了store. 組件終於獲取到store, 那就要從store中獲取state,注入組件中,那組件就要聲明一個狀態 allState 來接收store中的state, 同時在componentDidMounted 的時候,調用setState 給它賦值
static contextType = storeContext; // 加靜態屬性contextType, 賦值為storeContext state = { allState: {} } componentDidMount() { let {store} = this.context; // this.context 獲取到store this.setState({ allState: store.getState() }) }
把h1 中0 改為從狀態獲取
<h1 id="counter">{this.state.allState.counter}</h1>
頁面中顯示為5,沒有問題,表明從store中獲取的狀態沒有問題。那就要給三個按鈕添加click 事件了,dispatch action 來改變狀態,那就添加三個函數。首先從redux.js中引入三個action , 然后聲明三個函數dipatch action, 最后就是給按鈕添加上click 事件。
import { add, minus, reset } from './redux'; ..... add = () => { let {store} = this.context; store.dispatch(add); } minus = () => { let {store} = this.context; store.dispatch(minus); } reset = () => { let {store} = this.context; store.dispatch(reset); } ... <button type="button" className="btn btn-primary" style={{marginRight: '10px'}} onClick={this.add}>Add</button> <button type="button" className="btn btn-success" style={{marginRight: '10px'}} onClick={this.minus}>Minus</button> <button type="button" className="btn btn-danger" onClick={this.reset}>Reset</button>
點擊了按鈕,頁面的狀態並沒有刷新,那就是沒有subscribe 監聽狀態的改變, 還是在componentDidMounted 頁面里面調用store.subscribe,它的回調函數也很簡單,就是獲取狀態,調用setState()
componentDidMount() { let {store} = this.context; // this.context 獲取到store this.setState({ allState: store.getState() }) store.subscribe(() => { this.setState({ allState: store.getState() }) }) }
至此,react 和redux 算是接合成功了。整個threeButton.js 如下
import React, { Component } from 'react'; import { storeContext } from './storeContext'; // 引入storeContext import { add, minus, reset } from './redux'; export default class ThreeButton extends Component { static contextType = storeContext; // 加靜態屬性contextType, 賦值為storeContext state = { allState: {} } componentDidMount() { let {store} = this.context; // this.context 獲取到store this.setState({ allState: store.getState() }) store.subscribe(() => { this.setState({ allState: store.getState() }) }) } add = () => { let {store} = this.context; store.dispatch(add); } minus = () => { let {store} = this.context; store.dispatch(minus); } reset = () => { let {store} = this.context; store.dispatch(reset); } render() { return ( <div style={{textAlign: "center"}}> <h1 id="counter">{this.state.allState.counter}</h1> <button type="button" className="btn btn-primary" style={{marginRight: '10px'}} onClick={this.add}>Add</button> <button type="button" className="btn btn-success" style={{marginRight: '10px'}} onClick={this.minus}>Minus</button> <button type="button" className="btn btn-danger" onClick={this.reset}>Reset</button> </div> ) } }
現在回想一下組件中獲取store, dipatch aciton ,和實現實時監聽的步驟, 你會發現當我們再創建另外一個組件的時候,它也有好多相同的步驟 ,
1 ,添加靜態屬性contextType,使我們整個組件都能夠獲取到store, 肯定相同
2, 添加state來接受store中的state, 肯定相同。
3,componentDidMounted 下獲取state ,監聽state, 肯定相同
4, dispatch action, 這個幾乎不相同,因為每一個組件觸發的action 不同
5,render state, 就是頁面的ui, 這個也幾乎不同。
我們可以把這個組件分為三個部分,相同的部分不動,那不同的部分要怎么處理?對於不同的部分,通常都是使用函數,不同的部分通過傳參的形式傳遞進來,那就要寫一個函數,返回這個組件。由於action 和ui 是兩個不同類型的東西,可以分為兩種不同的參數,那這個函數接受action 返加一個函數,返加函數再接受一個ui 組件,再返回一個組件,這個組件包含相同的部分。相當於
function connect(action) { return function(Componnet) { return class extends React.componnet { // 相同的部分 render() { return <Componnet {...this.state}></Componnet> } } } }
只要把這個函數封裝起來,以后直接調用這個函數,就實現了組件自動獲取到store, 自動監聽變化,我們只要寫ui 和action,然后傳遞進去就可以了。這個函數其實第三方組件已經封裝好了,那就是react-redux 庫,它提供了一個connect方法, 看一下它的api, 最常用的就是下面的方式
connect(mapStateToProps, mapDispatchToProps)(MyComponent)。
和我們自己寫的connect 函數使用方法一致, 只不過它的第一個函數可以接受更多的參數,mapStateToProps, 把state 轉化成props,因為connect 返回的組件中能夠獲取到store中的state, 它要把state 傳遞給myComponnet, 因為myCompoent 才是負責渲染ui, 對於myComponnet 來說,它就是props. 同理也適用於mapDispatchToProps, 在connect的組件它是能獲取到store中的dispatch, 當傳遞給myComponent的時候,它就變成了Props. 再看一下這兩個參數怎么使用,首先它們是函數,然后返回對象。 為什么要這樣設計呢?只有函數,才能調用,才能通過參數把state和disptch 進行注入,返回對象,便於對象的合並,把所有對象進行合並,形成props 傳遞給myComponnet.
對於mapStateToProps 來說,它接受一個state作為參數,返回一個對象,這個對象中的屬性就可以在myComponet中使用props 進行獲取並使用,值呢?就是 參數state中的屬性,myComponent 組件要用到state中的哪個屬性,就讀取state中的哪個屬性作為參數。
const mapStateToProps = state => ({ counter: state.counter })
mapDispatchToProps,則相對麻煩一點,它接受一個dispatch 作為參數,返回的對象中屬性也是可以在myComponet中使用props 進行獲取並使用,值呢,是一個函數,參數可以接受也可以不接受,函數體則是dispatch action
const mapDispatchToProps = dispatch => ({ add: () => dispatch({type: 'ADD'}) })
React-Redux 除了connect 函數外,還提供了Provider 組件,和我們自定義的storeContext.Provider 一致,不過它的使用方式是直接提供屬性,組件身上的屬性都能被子組件獲取到。npm i react-redux --save 使用react-redux 重寫組件。
首先把storeContext.js 文件去掉,然后在index.js中從React-Redux引入Provider 組件,包含App
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import 'bootstrap/dist/css/bootstrap.css'; // 添加bootstrap 樣式 import { store } from './redux'; // 引入 store import { Provider } from 'react-redux'; // 提供sotre 作為共享數據,App及其子組件都能獲取到store const ProviderWrapper = <Provider store={store}> <App /> </Provider> ReactDOM.render(ProviderWrapper, document.getElementById('root'));
然后ThreeButton.js 就要分為兩部分了,一個部分是connect 函數中的第一部分connect(mapStateToProps, mapDispatchToProps), 主要的作用就是把store 獲取,轉化為props.
另一部分是connect的第二部分myComponent, 它呢,就是接受到props, 渲染組件。 在一個文件中也可以,分兩個文件也沒有問題。我們就在一個文件中寫了,
import React from 'react'; import { add, minus, reset } from './redux';
// 純渲染組件 function ThreeButton(props) { return ( <div style={{textAlign: "center"}}> <h1 id="counter">{props.counter}</h1> <button type="button" className="btn btn-primary" style={{marginRight: '10px'}} onClick={props.add}>Add</button> <button type="button" className="btn btn-success" style={{marginRight: '10px'}} onClick={props.minus}>Minus</button> <button type="button" className="btn btn-danger" onClick={props.reset}>Reset</button> </div> ) } // 把store中的state 轉化為純渲染組件props const mapStateToProps = state => ({ counter: state.counter }) // 獲取store中的dispatch,同時和action接合,組成純渲染組件props,渲染組件中,直接調用對象的屬性,就可以dispatch action 了 const mapDispatchToProps = dispatch => ({ add: () => dispatch(add), minus: () => dispatch(minus), reset: () => dispatch(reset) }) // connect 函數把它們接合起來,ThreeButton就可以使用props來使用mapStateToProps和mapDispatchToProps中返回的對象屬性 // 同時返回一個組件,可以在父組件App.js 中直接調用 export default connect( mapStateToProps, mapDispatchToProps, )(ThreeButton)