Redux 和React 結合


  當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
 import { storeContext } from './storeContext'; // 引入storeContext

// 提供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';
 import { connect } from 'react-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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM