從零開始學React:四檔(下篇)一步一步學會react-redux

Mr.奇淼發表於2019-02-13

手挽手帶你學React入門四檔,用人話教你react-redux,理解redux架構,以及運用在react中。學完這一章,你就可以開始自己的react專案了。

視訊教程

上一篇我們自己實現了Redux,這一篇我們來看看如何去實現一個react-redux

開始之前

本文需要用到的知識

首先你要會React的基礎(這是廢話) 高階元件 React context 滿足這三項我們開始往下看。

react結合redux


搭建基礎環境

我們上一章講過了redux的原理,內部是有一個store的,store只有dispatch才可以控制它變化。還有在文章一開始我們講了React context 可以跨元件傳遞資料,那麼到現在你想到把我們的store掛到最外層一個元件的context上了嗎?話不多說 開始!

我們先修改一下我們的react檔案(注意不是redux.html這個檔案了)

    // App.js
import React,{Component} from 'react'
import PropTypes from 'prop-types'  //引入

export default class App extends Component {
    constructor(){
        super()
        this.state={
          
        }
    }
    componentWillMount(){
        // console.log(hashHistory)
    }
    render() {
        return (
            <div>
                <Children />
                <ChildrenTwo />
            </div>
        )
    }
  
}

// 為了展示效果定義子元件一

class Children extends Component{
    constructor(){
        super()
        this.state={
            
        }
    }
    render(){
        return(
            <div>
                <h1>我是腦袋</h1>
                <h2>我是身體</h2>
            </div>
        )
    }
}

// 為了展示效果定義子元件二 ChildrenTwo 是 Children的子元件 但是卻通過context拿到了App元件拿過來的值 (越級傳遞)

class ChildrenTwo extends Component{
    constructor(){
        super()
        this.state={
        
        }
    }
    render(){
        return(
            <div>
                <button>變字</button>
                <button>變色</button>
            </div>
        )
    }
}

複製程式碼

建立基本store

現在我們做好了示例檔案的基礎模板了,然後我們需要建立一個store.js


// store.js

// 這是我們的 reducer
const changeDom = (state,action) => {
    if(!state)return{
        text : "我是例項文字",
        color : 'red'
    }
    switch(action.type){
        case "CHANGE_TEXT":
        return{
            ...state,
            text:action.text
        }
        case "CHANGE_COLOR":
        return{
            ...state,
            color:action.color
        }
        default:
        return state
    }
}


const creatStore = (reducer)=>{
    let state = null
    const listeners = []
    const subscribe = (liestner)=>listeners.push(liestner)
    const getState = ()=>state
    const dispatch=(action)=>{
        state = reducer(state,action)
        listeners.map(item=>item())
    }
    dispatch({})
    return { getState, dispatch, subscribe }
}
export {creatStore,changeDom}
複製程式碼

結合context使用store

我們現在把我們的子元件通過context公用App的store,並且把渲染方法和dispatch應用進去


    // App.js
    import React,{Component} from 'react'
    import PropTypes from 'prop-types'  //引入
    
    // 引入 store
    import {changeDom,creatStore} from './store'

    const store = creatStore(changeDom)

    export default class App extends Component {
        // 我們在這裡把store掛到context上
        static childContextTypes = {
            store: PropTypes.object
        }
        getChildContext(){
            return{store}
        }
        constructor(){
            super()
            this.state={
              
            }
        }
        componentWillMount(){
            // console.log(hashHistory)
        }
        render() {
            return (
                <div>
                    <Children />
                    <ChildrenTwo />
                </div>
            )
        }
      
    }
    // 這裡我們再去修改我們的子孫組建 讓他們可以拿到 store
    
    // 為了展示效果定義子元件一
    
    class Children extends Component{
        static contextTypes = {
            store: PropTypes.object
          }
        constructor(){
            super()
            this.state={
                color:"",
                text:""
            }
        }

        // 這裡我們定義兩個渲染方法
        getColor(){
            let store = this.context.store.getState()
            this.setState({color:store.color})
        }

        // 這裡我們定義兩個渲染方法

        getText(){
            let store = this.context.store.getState()
            this.setState({text:store.text})
        }

        // 這裡元件載入之前渲染
        componentWillMount(){
            this.getColor()
            this.getText()
        }
        render(){
            return(
                <div>
                    <h1 style={{color:this.state.color}}>{this.state.text}</h1>
                </div>
            )
        }
    }
    
    // 為了展示效果定義子元件二 ChildrenTwo 是 Children的子元件 但是卻通過context拿到了App元件拿過來的值 (越級傳遞)
    
    class ChildrenTwo extends Component{
        static contextTypes = {
            store: PropTypes.object
          }
        constructor(){
            super()
            this.state={
            
            }
        }
        // 這裡我們定義 兩個 action
        changeColor=()=>{
            this.context.store.dispatch({type:"CHANGE_COLOR",color:"green"})
            console.log(this.context.store.getState())
        }
        changeText=()=>{
            this.context.store.dispatch({type:"CHANGE_TEXT",text:"我變了"})
            console.log(this.context.store.getState())  //這裡方便大家看到資料變了
        }
        render(){
            return(
                <div>
                    <button onClick={()=>{this.changeText()}}>變字</button>
                    <button onClick={()=>{this.changeColor()}}>變色</button>
                </div>
            )
        }
    }
    
複製程式碼

顯然 現在我們點選按鈕的時候,並沒有發生任何事情,可是我們看console可以看到,store的資料確實變化了,這是為什麼呢?很簡單 我們沒有把dom監聽放進來,下一步我們要設定監聽。

設定dom監聽

    // Children元件 我們在componentWillMount 中新增監聽
    class Children extends Component{
        static contextTypes = {
            store: PropTypes.object
          }
        constructor(){
            super()
            this.state={
                color:"",
                text:""
            }
        }

        // 這裡我們定義兩個渲染方法
        getColor(){
            let store = this.context.store.getState()
            this.setState({color:store.color})
        }

        // 這裡我們定義兩個渲染方法

        getText(){
            let store = this.context.store.getState()
            this.setState({text:store.text})
        }

        // 這裡元件載入之前渲染
        componentWillMount(){
            this.getColor()
            this.getText()
            this.context.store.subscribe(()=>{this.getColor()})
            this.context.store.subscribe(()=>{this.getText()})// 使用箭頭函式 保證this指向

        }
        render(){
            return(
                <div>
                    <h1 style={{color:this.state.color}}>{this.state.text}</h1>
                </div>
            )
        }
    }
複製程式碼

到這裡我們已經簡單地實現了react-redux,可是大家有沒有覺得怪怪的?

開始建立connect

沒錯 到這裡我們頻繁使用context,並且每個元件都要去手動掛context 這是相當麻煩的,現在我們想要讓這些東西都自動包裹 自動生成,我們再怎麼做呢。 我們建立一個高階元件就可以搞定這個問題了(一個函式,接收一個元件,生成另外一個包裝好的新元件)


import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (WrappedComponent)=>{
    class Connect extends Component{
        static contextTypes = {
            store: PropTypes.object
        }
        render(){
            return<WrappedComponent />
        }
    }
    return Connect
}

複製程式碼

我們這裡通過 connect函式來接收一個元件 經過一層封裝 返回一個包裝了context的元件 但是現在這樣的話 我們每次使用都還需要大量的書寫 this.context 是不是相當噁心呢? 並且每個元件都拿到了store 有很多事它並不需要的,這時候我們就需要告訴這個高階元件,我這裡就只需要這些東西。這就是 mapStateToProps


import React, { Component } from 'react'
import PropTypes from 'prop-types'

// 示例 這裡不用
// const mapStateToProps=(state)=>{
//     return{
//         color:state.color,
//         text:state.text,
//     }
// }  就是我們需要從store拿出來的東西

const connect= (mapStateToProps) => (WrappedComponent)=>{
    class Connect extends Component{
        static contextTypes = {
            store: PropTypes.object
        }
        render(){
            const store = this.context.stote
            let stateProps = mapStateToProps(store.getState())
            然後我們把stateProps用...展開
            return<WrappedComponent {...stateProps}/>
        }
    }
    return Connect
}

複製程式碼

現在我們利用Connect改造我們的元件


    // App.js
    import React,{Component} from 'react'
    import PropTypes from 'prop-types'  //引入
    
    // 引入 store
    import {changeDom,creatStore} from './store'

    const store = creatStore(changeDom)

// ________________________________把connect_____________________________寫在app.js 為的是不引入了 實際上它是單獨拆分在react-redux中的
    const connect = (mapStateToProps) => (WrappedComponent)=>{
        class Connect extends Component{
            static contextTypes = {
                store: PropTypes.object
            }
            render(){
                const store = this.context.store
                console.log(store)
                let stateProps = mapStateToProps(store.getState())
                // 然後我們把stateProps用...展開
                return<WrappedComponent {...stateProps}/>
            }
        }
        return Connect
    }
// ________________________________把connect_____________________________寫在app.js 為的是不引入了 實際上它是單獨拆分在react-redux中的

// app.js 的其他內容...

// ________________________________對元件一進行修改_____________________________
    
    class Children extends Component{
        constructor(){
            super()
            this.state={
                color:"",
                text:""
            }
        }

        render(){
            return(
                <div>
                    <h1 style={{color:this.props.color}}>{this.props.text}</h1>
                </div>
            )
        }
    }

    const mapStateToProps = (state) => {
        return {
            color: state.color,
            text:state.text
        }
    }
    Children = connect(mapStateToProps)(Children)
// ________________________________對元件一進行修改_____________________________


複製程式碼

現在我們成功把store裡面的所有東西都掛到了props上面,我們不需要去依賴context,只需要呼叫props即可。剩下的就是我們的資料變更,渲染還有監聽了!

對 connect 進一步改造 其實就是像我們上面的元件一樣 掛載監聽

    const connect = (mapStateToProps) => (WrappedComponent)=>{
        class Connect extends Component{
            static contextTypes = {
                store: PropTypes.object
            }
            componentWillMount(){
                const{store} = this.context
                this.updata()
                store.subscribe(()=>this.updata())
            }
            updata=()=>{
                 const { store } = this.context
                 let stateProps = mapStateToProps(store.getState(), this.props)
                  this.setState({
                        allProps: { // 整合普通的 props 和從 state 生成的 props
                        ...stateProps,
                        ...this.props
                        }
                    })
            }
            render(){
                // 然後我們把allProps用...展開
                return<WrappedComponent {...this.state.allProps}/>
            }
        }
        return Connect
    }

複製程式碼

mapDispatchToProps

state我們改造完了,dispatch 是不是也要一起改造呢?答案是肯定的,mapDispatchToProps就是做這個的。 我們看看 mapDispatchToProps 是怎麼寫的 我們倒著推


//const mapDispatchToProps = (dispatch) => {  // 傳入的是 dispatch
//  return {  //返回一個函式
//    changeColor: (color) => {
//      dispatch({ type: 'CHANGE_COLOR', color: color })
//    }
//  }
//}


const connect = (mapStateToProps,mapDispatchToProps) => (WrappedComponent)=>{
    class Connect extends Component{
        static contextTypes = {
            store: PropTypes.object
        }
        componentWillMount(){
            const{store} = this.context
            this.updata()
            console.log(store)
            store.subscribe(()=>this.updata())
        }
        updata=()=>{
             const { store } = this.context
            let stateProps = mapStateToProps?mapStateToProps(store.getState(), this.props):{}
             let dispatchProps = mapDispatchToProps?mapDispatchToProps(store.dispatch,this.props):{}
             // 我們要考慮到空值處理
              this.setState({
                    allProps: { // 整合普通的 props 和從 state 生成的 props
                    ...stateProps,
                    ...this.props
                    ...dispatchProps,
                    }
                })
        }
        render(){
            // 然後我們把allProps用...展開
            return<WrappedComponent {...this.state.allProps}/>
        }
    }
    return Connect
}

複製程式碼

到這裡我們可以把ChildrenTwo的程式碼也改造了


    
    class ChildrenTwo extends Component{
        constructor(){
            super()
            this.state={
            
            }
        }
        render(){
            console.log(this.props)
            return(
                <div>
                    <button onClick={()=>{this.props.changeText("我變了")}}>變字</button>
                    <button onClick={()=>{this.props.changeColor("green")}}>變色</button>
                </div>
            )
        }
    }
    const mapDispatchToProps = (dispatch)=>{
        return {  //返回一個函式
                changeColor: (color) => {
                    dispatch({ type: 'CHANGE_COLOR', color: color })
                },
                changeText:(text)=>{
                    dispatch({ type: 'CHANGE_TEXT', text: text })
                }
            }
    }
    ChildrenTwo = connect(null,mapDispatchToProps)(ChildrenTwo)

複製程式碼

到現在 一個簡單的 react-redux已經差不多了,但是react-redux裡面還有一個<Provider> 但是我們還沒有這東西 這是什麼呢?實際上它做的事跟我們App這個元件做的事一毛一樣,主要就是把store掛到context上。

    export class Provider extends Component{
        static childContextTypes = {
            store: PropTypes.object
        }
        getChildContext(){
            return{store}
        }
        render() {
            return (
                <div>{this.props.children}</div>
            )
        }
    }
複製程式碼

於是 我們現在的程式碼變成了這樣

好了 道理講完了 我們現在開始拆分程式碼啦。

// store.js
export const creatStore = (reducer)=>{
    let state = null
    const listeners = []
    const subscribe = (liestner)=>listeners.push(liestner)
    const getState = ()=>state
    const dispatch=(action)=>{
        state = reducer(state,action)
        listeners.map(item=>item())
    }
    dispatch({})
    return { getState, dispatch, subscribe }
}

複製程式碼
// reducer.js

// 這是我們的 reducer  可以單獨拆分成一個js檔案 自己拆吧
export const changeDom = (state,action) => {
    if(!state)return{
        text : "我是例項文字",
        color : 'red'
    }
    switch(action.type){
        case "CHANGE_TEXT":
        return{
            ...state,
            text:action.text
        }
        case "CHANGE_COLOR":
        return{
            ...state,
            color:action.color
        }
        default:
        return state
    }
}
複製程式碼
// react-redux.js
import React,{Component} from 'react'
import PropTypes from 'prop-types'  //引入

export const connect = (mapStateToProps,mapDispatchToProps) => (WrappedComponent)=>{
    class Connect extends Component{
        static contextTypes = {
            store: PropTypes.object
        }
        componentWillMount(){
            const{store} = this.context
            this.updata()
            store.subscribe(()=>this.updata())
        }
        updata=()=>{
             const { store } = this.context
             let stateProps = mapStateToProps?mapStateToProps(store.getState(), this.props):{}
             let dispatchProps = mapDispatchToProps?mapDispatchToProps(store.dispatch,this.props):{}

             // 做一下空值處理
              this.setState({
                    allProps: { // 整合普通的 props 和從 state 生成的 props
                    ...stateProps,
                    ...this.props,
                    ...dispatchProps,
                    }
                })
        }
        render(){
            // 然後我們把allProps用...展開
            return<WrappedComponent {...this.state.allProps}/>
        }
    }
    return Connect
}


export class Provider extends Component{
        static childContextTypes = {
            store: PropTypes.object
        }
        getChildContext(){
            return {store:this.props.store}
        }
        render() {
            return (
                <div>{this.props.children}</div>
            )
        }
}

複製程式碼

// App.js
import React,{Component} from 'react'
import Children from './Children'
import ChildrenTwo from './ChildrenTwo'
export default class App extends Component {
    constructor(){
        super()
        this.state={
          
        }
    }

    render() {
        return (
            <div>
                <Children />
                <ChildrenTwo />
            </div>
        )
    }
}
複製程式碼
// Children.js
import React,{Component} from 'react'
import{connect} from './react-redux.js'
class Children extends Component{
    constructor(){
        super()
        this.state={
            color:"",
            text:""
        }
    }

    render(){
        return(
            <div>
                <h1 style={{color:this.props.color}}>{this.props.text}</h1>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        color: state.color,
        text:state.text
    }
}
Children = connect(mapStateToProps)(Children)

export default Children
複製程式碼
// ChildrenTwo.js

import React,{Component} from 'react'
import{connect} from './react-redux.js'
class ChildrenTwo extends Component{
        constructor(){
            super()
            this.state={
            
            }
        }
        render(){
            return(
                <div>
                    <button onClick={()=>{this.props.changeText("我變了")}}>變字</button>
                    <button onClick={()=>{this.props.changeColor("green")}}>變色</button>
                </div>
            )
        }
    }
    const mapDispatchToProps = (dispatch)=>{
        return {  //返回一個函式
                changeColor: (color) => {
                    dispatch({ type: 'CHANGE_COLOR', color: color })
                },
                changeText:(text)=>{
                    dispatch({ type: 'CHANGE_TEXT', text: text })
                }
            }
    }
    ChildrenTwo = connect(null,mapDispatchToProps)(ChildrenTwo)
    
    export default ChildrenTwo 

複製程式碼

拆完了的程式碼是不是簡單明瞭 真正工作裡面 我們需要多做兩步

npm i redux --save
npm i react-redux --save
複製程式碼

然後 我們 對照著修改這幾個位置

// creatStore 在 redux 外掛中
// connect,Provider 在 react-redux 外掛中
// 也就是用到哪裡了 對應修改哪裡 改完了你就發現了新大陸了
import { createStore } from 'redux'
import { connect,Provider } from 'react-redux'
複製程式碼

不知不覺發現自己不僅僅會用了react-redux 並且還自己實現了一個react-redux 很舒坦的呢

總結

在我們四檔下篇到這裡結束了,這就是react-redux的實現和寫法,大家自己去實現真正的寫法,這裡不做演示相當於給大家留個作業,有錯誤或者是有建議 或者有不懂的地方 掃碼加我微信給大家解答。

視訊製作中

相關文章