Redux实例学习之计数器

创建counter项目

create-react-app counter

删除多余文件,src目录只保留app.js和index.js

index.js内容如下:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
    <App/>,
    document.getElementById('root')
);

app.js内容如下:

import React from 'react'

export default class App extends React.Component {
    increase = () => {
        console.log('+1')
    }
    decrease = () => {
        console.log('-1')
    }

    render() {
        return (
            <div>
                <p><span style={{fontSize: '50px'}}>0</span></p>
                <p>
                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.increase}> +
                    </button>
                     
                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.decrease}> -
                    </button>
                </p>
            </div>
        );
    }
}

安装redux

yarn add redux

安装后,启动:yarn start,运行效果如图:

得到当前状态

目前页面上显示的数字是写死的,而实际应该从状态中获取,这样方便我们做加减法时修改状态,使页面上的数字得到自动更新。
redux的状态都是保存在store对象之中的,所以我们要做的第一步是引入redux,并创建store对象。
app.js第二行添加如下代码:

import {createStore} from 'redux'

Redux 提供createStore这个函数,用来生成Store

  • Store
    Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。

接着在下一行创建store对象:

const store = createStore((state,action)=>{
    console.log('state:', state)
    console.log('action:', action)
    return state
})

createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。接受的函数中有两个入参,state和action,state代表当前的状态,action为当前的动作

当前时刻的State,可以通过store.getState()拿到。

当刷新页面时,观察控制台,可以看到如下结果:

可以看到,页面在渲染后,打印了stateaction两个参数,其中state的值为undefinedaction为一个对象,该对象有一个type属性,值为@@redux/INITe.m.x.f.8.p
如果我们希望state的初始值是0,那么可以给state这个参数加上默认值。

const store = createStore((state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    return state
})

再次刷新页面,可以看到控制台结果如下:

  • Action
    Action 是一个对象,该对象包含多个属性,其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。
    Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store

现在我们把页面中(app.js)写死的0替换为当前状态
将以下代码

<p><span style={{fontSize: '50px'}}>0</span></p>

替换为:

<p><span style={{fontSize: '50px'}}>{store.getState()}</span></p>

此时,页面上显示的数字0就是从状态中获取的了。

改变状态

现在我们希望点击加号后,改变状态值,使其结果变为1。
前面说了,改变 State 的唯一办法,就是使用 Action
increase函数中,创建一个action对象,内容如下:

const action = {
    type: 'increment',
    number: 1
}

这个action包含了两个属性,其中type属性是必须的,内容为increment,代表当前要执行的是这个动作。number是我们自己取的属性名(可以改成其他的),值为1,代表执行加这个动作后,当前状态需要加上1。
前面提到所有的数据都保存在store对象,要完成加法,需要先从store中取得当前状态值,并加上action.number的值,最后把结果存入当前状态。
与之前react通过this.setState来更新状态不同,store并没有setState这个方法。store提供了dispatch方法来把action传递出去。

increase = () => {
    const action = {
        type: 'increment',
        number: 1
    }
    store.dispatch(action)
}

现在点击页面上的加号按钮,观察控制台,可以看到如下结果:

可以看到,当点击了加号后,action对象被传递到了createStore接收的函数参数这里。这个函数被称为Reducer

  • Reducer
    Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer
    Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State
    将以下代码:
const store = createStore((state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    return state
})

改写为:

const reducer = (state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    return state
}
const store = createStore(reducer)

然后根据actiontype属性来决定是做加法还是减法,根据number属性来决定加多少和减多少。
进一步完善代码为:

const reducer = (state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    switch (action.type) {
        case 'increment':
            return state + parseInt(action.number)
        case 'decrement':
            return state - parseInt(action.number)
        default:
            return state
    }
}

刷新网页,多次点击加号,可以看到控制台有如下结果:

可以看到state已发生了变化。

监听状态变化

观察控制台可以看到每次点击加号后,state都发生了变化,但是页面上的数字却还是0,没有变化。这时需要调用storesubscribe方法来设置监听,一旦state发生变化,就自动执行这个函数。
在以下代码:

const store = createStore(reducer)

下方添加如下代码:

store.subscribe(() => {
    console.log('监听状态:',store.getState())
})

然后刷新网页,点击加号,观察控制台:

可以看到每次点击后,state发生了变化的同时,redux都会自动调用store的subscribe方法。也就是说,我们只需要把ReactDOM.render写在subscribe方法里就可以实现state更新后view的自动刷新了。

根据状态自动刷新

由于ReactDOM.render是写在index.js中的,所以store对象也应该写到index.js里。
改写后的index.js代码如下:

import React from 'react'
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import App from './App'

const reducer = (state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    switch (action.type) {
        case 'increment':
            return state + parseInt(action.number)
        case 'decrement':
            return state - parseInt(action.number)
        default:
            return state
    }
}

const store = createStore(reducer)

ReactDOM.render(
    <App store={store}/>,
    document.getElementById('root')
)

store.subscribe(() => {
    ReactDOM.render(
        <App store={store}/>,
        document.getElementById('root')
    )
})

通过<App store={store}/>像app.js传递store对象。
改写后的app.js内容如下:

import React from 'react'

export default class App extends React.Component {
    increase = () => {
        const {store} = this.props
        const action = {
            type: 'increment',
            number: 1
        }
        store.dispatch(action)
    }

    decrease = () => {
        const {store} = this.props
        const action = {
            type: 'decrement',
            number: 1
        }
        store.dispatch(action)
    }

    render() {
        const {store} = this.props
        return (
            <div>
                <p><span style={{fontSize: '50px'}}>{store.getState()}</span></p>
                <p>
                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.increase}> +
                    </button>
                     

                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.decrease}> -
                    </button>
                </p>
            </div>
        );
    }
}

现在可以根据状态自动刷新网页了,效果如图:

使用react-redux

概念

react-redux是一个react插件库,专门用来简化react应用中使用redux。

安装:yarn add react-redux

组件分类

React-Redux将所有组件分成两大类

  • 1)UI组件
    a.只负责 UI 的呈现,不带有任何业务逻辑
    b.通过props接收数据(一般数据和函数)
    c.不使用任何 Redux 的 API
    d.一般保存在components文件夹下
  • 2)容器组件
    a.负责管理数据和业务逻辑,不负责UI的呈现
    b.使用 Redux 的 API
    c.一般保存在containers文件夹下

在src目录下,新建components文件夹和containers文件夹,其中components文件夹用于放UI组件,containers文件夹用于放容器组件。

UI组件:Counter

components文件夹下新建Counter.jsx,内容如下:

import React from 'react'
import PropTypes from 'prop-types'

/**
 * UI组件
 */
export default class Counter extends React.Component {
    static propTypes = {
        count: PropTypes.number.isRequired,
        increment: PropTypes.func.isRequired,
        decrement: PropTypes.func.isRequired
    }
    increase = () => {
        this.props.increment(1)
    }

    decrease = () => {
        this.props.decrement(1)
    }

    render() {
        const {count} = this.props
        return (
            <div>
                <p><span style={{fontSize: '50px'}}>{count}</span></p>
                <p>
                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.increase}> +
                    </button>
                     

                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.decrease}> -
                    </button>
                </p>
            </div>
        );
    }
}

通过PropTypes限定了该组件依赖于countincrementdecrement三个属性,其中count用于存放点击加减后的结果,是一个数字类型,incrementdecrement则为函数类型,用于实现具体的加法和减法操作。这三个属性均为父组件传递过来的,该组件并不关心实现过程,只负责调用和显示。

容器组件:App

containers文件夹下新建App.jsx,该组件通过包装UI组件生成容器组件,react-redux提供了connect高阶函数来实现,语法为:

connect(
    mapStateToprops,
    mapDispatchToProps
  )(Counter)

connect函数接收两个参数,mapStateToprops函数用于将特定state数据映射(转换)成标签的一般属性传递给UI组件(Counter),mapDispatchToProps函数用于将包含dispatch函数调用语句的函数映射(转换)成标签的函数属性传递给UI组件(Counter)。

属性分为两大类:函数属性、一般属性(不是函数的都是一般属性)
函数属性为子向父通信
一般属性为父向子通信

App.jsx完整内容如下:

import {connect} from 'react-redux'
import Counter from '../components/counter'

/**
 * 应用根组件:通过包装UI组件生成容器组件
 */

/**
 * 将特定state数据映射(转换)成标签的一般属性传递给UI组件(Counter)
 * redux在调用此函数时,传入了store.getState()的值
 */
const mapStateToprops = (state) => {
    //返回的对象的所有属性传递给UI组件
    return {
        count: state
    }
}

/**
 * 将包含dispatch函数调用语句的函数映射(转换)成标签的函数属性传递给UI组件(Counter)
 * redux在调用此函数时,传入了store.dispatch的值
 */
const mapDispatchToProps = (dispatch) => {
    return {
        increment: (number) => dispatch({
            type:'increment',
            number
        }),
        decrement: (number) => dispatch({
            type:'decrement',
            number
        })
    }
}
export default connect(
    mapStateToprops,        //用来指定传递哪些一般属性
    mapDispatchToProps      //用来指定传递哪些函数属性
)(Counter)

mapStateToprops和mapDispatchToProps的返回值都是一个对象,该对象的所有属性都会传递到UI组件,使UI组件可以通过this.props获取到。

入口文件index.js

修改src目录下的index.js,删除之前的subscribe监听变化的代码,同时引入react-redux的Provider组件,Provider组件会将接收到的store对象提供给所有的容器组件

完整index.js内容如下:

import React from 'react'
import ReactDOM from 'react-dom'
import {createStore} from 'redux'
import {Provider} from 'react-redux'
import App from './containers/App'

const reducer = (state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    switch (action.type) {
        case 'increment':
            return state + parseInt(action.number)
        case 'decrement':
            return state - parseInt(action.number)
        default:
            return state
    }
}

const store = createStore(reducer)

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
)

App.jsx的简写

import {connect} from 'react-redux'
import Counter from '../components/counter'
const increment = (number) => ({
    type: 'increment',
    number
})
const decrement = (number) => ({
    type: 'decrement',
    number
})
export default connect(
    state => ({count: state}),
    {increment, decrement}
)(Counter)

redux 异步编程

安装redux插件(异步中间件)

yarn add redux-thunk

修改index.js

index.js做如下更改:

import {createStore, applyMiddleware} from 'redux'
import thunk from 'redux-thunk'
const store = createStore(reducer, applyMiddleware(thunk))

完整index.js代码如下:

import React from 'react'
import ReactDOM from 'react-dom'
import {createStore, applyMiddleware} from 'redux'
import {Provider} from 'react-redux'
import thunk from 'redux-thunk'
import App from './containers/App'

const reducer = (state = 0, action) => {
    console.log('state:', state)
    console.log('action:', action)
    switch (action.type) {
        case 'increment':
            return state + parseInt(action.number)
        case 'decrement':
            return state - parseInt(action.number)
        default:
            return state
    }
}

const store = createStore(reducer, applyMiddleware(thunk))

ReactDOM.render(
    <Provider store={store}>
        <App/>
    </Provider>,
    document.getElementById('root')
)

修改Counter.jsx

import React from 'react'
import PropTypes from 'prop-types'

/**
 * UI组件
 */
export default class Counter extends React.Component {
    static propTypes = {
        count: PropTypes.number.isRequired,
        increment: PropTypes.func.isRequired,
        decrement: PropTypes.func.isRequired,
        increaseAsync: PropTypes.func.isRequired,
    }
    increase = () => {
        this.props.increment(1)
    }

    decrease = () => {
        this.props.decrement(1)
    }

    increaseAsync = () => {
        this.props.increaseAsync(1)
    }

    render() {
        const {count} = this.props
        return (
            <div>
                <p><span style={{fontSize: '50px'}}>{count}</span></p>
                <p>
                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.increase}> +
                    </button>
                     

                    <button style={{fontSize: '30px', 'width': '50px', 'height': '50px'}} onClick={this.decrease}> -
                    </button>
                     

                    <button style={{fontSize: '30px', 'width': '170px', 'height': '50px'}} onClick={this.increaseAsync}> 1秒后增加
                    </button>
                </p>
            </div>
        );
    }
}

修改app.jsx

import {connect} from 'react-redux'
import Counter from '../components/counter'

/**
 * 应用根组件:通过包装UI组件生成容器组件
 */

const increment = (number) => ({
    type: 'increment',
    number
})
const decrement = (number) => ({
    type: 'decrement',
    number
})
const increaseAsync = (number) => {
    return dispath => {
        //1.执行异步代码
        setTimeout(() => {
            //2.分发action
            dispath(increment(number))
        }, 1000)
    }
}
export default connect(
    state => ({count: state}),
    {increment, decrement, increaseAsync}
)(Counter)

redux调试工具

1.谷歌浏览器安装Redux DevTools插件
2.安装工具依赖包

yarn add redux-devtools-extension --dev

3.编码

import {composeWithDevTools} from 'redux-devtools-extension'
const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))

4.安装后演示截图

发表评论

邮箱地址不会被公开。 必填项已用*标注