听风是风

学或不学,知识都在那里,只增不减。

导航

从零开始的react入门教程(十),快速上手react-redux,相对于redux它究竟简化了什么?

壹 ❀ 引

在前面两篇文章中,我们介绍了reduxcontext部分概念与基本用法,这里我们做个简单复习。

redux属于应用数据流框架,主要用于应用状态的管理,比如react中的state。其数据流为view-->action-->reducer-->store-->view,比如用户点击了一个按钮,本质触发的是store.dispatch(action),然后reducer感知事件,触发actionType对应的更新数据方法,从而达到更新store的目的,而当store更新后,早在view订阅的store.subscribe又被触发,这样又通过store.getState拿到最新的store并将其设置为组件的statestate发生变化自然会让组件重新render,这便是一次完整的数据更新过程。

而随之问题也暴露出来了,多个组件需要使用store的数据都得引入store.js文件,而且dispatch以及subscribe等API都在store上,所以你需要使用这些方法的地方也一样得提前引入store。于是,我们紧接着介绍了context概念,context(上下文)的作用主要用于解决组件跨级传值问题,比如上面提到的API方法,我们就可以通过context.Providervaluestore传递下去,这样不管哪个组件都可以通过类似this.context.dispatch的写法调用store上的方法。除了全局传递外,比如A-->B-->C-->D场景,D需要拿到A的数据,但是又不希望BC作为数据传递的工具人,使用context.Consumer同样能解决这一类场景的问题。

接下来要介绍的react-reduxredux作者为react量身定制的库,使用上进一步简化了我们对于redux以及context的写法。由于react-redux是独立redux的存在,因此我们需要单独引入它。在项目根目录执行npm install --save react-redux,接下来我们来了解其在用法的变化,没关系,有前两篇文章的铺垫,这并不会很难!本文开始。

贰 ❀ react-redux

react-redux提供了ProviderconnectmapStateToPropsmapDispatchToProps四个API,我们来一一介绍它们。

贰 ❀ 壹 Provider

在之前context中我们使用Provider得先通过React.createContext()创建,不过有了react-redux后我们可以直接引用,比如:

import { Provider } from 'react-redux'

其用法与含义与之前完全相同,我们还是使用之前文章的例子,在index中做部分修改,将Provider的引用改为react-redux

import React, { Component } from 'react';
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom';
import store from './Store.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
class ControlPanel extends Component {
    render() {
        return (
            <div>
                <Counter caption="First" />
                <Counter caption="Second" />
                <hr />
                <Summary />
            </div>
        );
    }
}
ReactDOM.render(
    <Provider value={store}>
        <ControlPanel />
    </Provider>,
    document.getElementById('root')
);

注意,这次我们把Provider用在了ReactDOM.render中,也就是进行了真正意义上的全局包裹,之前文章的例子由于ControlPanel组件自身没有使用到store的数据,而是它的子组件需要用,所以之前的写法如下,也没有什么问题:

class ControlPanel extends Component {
    render() {
        return (
          	//我们使用了Provider包裹子组件,通过value传递store
            <context.Provider value={store}>
                <div>
                    <Counter caption="First" />
                    <Counter caption="Second" />
                    <hr />
                    <Summary />
                </div>
            </context.Provider>
        );
    }
}
ReactDOM.render(
    <ControlPanel />,
    document.getElementById('root')
);

这里只是做个写法纠正说明,并不是react-redux特性如此必须这么写,所以单独做个说明。保存后运行项目会报错,毕竟后续还有代码还没改完,我们先这样。

贰 ❀ 贰 connect

上面的代码修改,我们为全局上下文中添加了数据store,还记得在之前context介绍中如何使用上下文中的数据吗?两种方式,第一种是通过conponentName.contextType = context先为当前组件绑定上下文,之后在constructor中通过super(context)引入,之后就可以通过this.context访问上下文了,第二种方式通过Consumer中回调函数形参直接访问。

而在react-redux中我们通过connect方法帮助组件链接全局上下文,比如:

import { connect } from 'react-redux';
export default connect(mapStateToProps, mapDispatchToProps)(component);

上面代码中component就是你当前创建的组件,由于这个组件没有与上下文扯上关系,所以这里我们使用connect帮助它链接全局store,其次connect接受两个参数,分别是mapStateToPropsmapDispatchToProps,有什么用我们后面再介绍。

这里我们补充一个概念,react-redux将组件分为UI组件容器组件UI组件很好理解,只负责view渲染,不管理state变化,不管理业务逻辑,也不使用redux的API,它所接受的一切数据方法均由props提供。

而所谓容器组件作用与UI组件互补,它的工作是负责state变更以及业务逻辑处理,而这里的容器组件其实由react-redux生成。

注意上面connect这行代码,它本质上等同于:

// 帮UI组件注入数据方法,于是得到了一个新的容器组件,connect就像给一部手机通了电一样,让其有了生命力
const 容器组件 = connect(mapStateToProps, mapDispatchToProps)(UI组件);
export 容器组件;

也就是说我们通过connect帮助一个UI组件链接到了数据层,这里的数据可以是全局的store,也可以是上层组件传递下来的props。于是我们得到了一个被注入了数据的新组件。

说到这里,不知道大家能不能感受到这种做法与context使用全局store的差异性,context使用数据要么直接把上下文与组件绑定后使用,要么借用Consumer使用,就像被内嵌进组件一样,耦合度较高。而connect更像在组件外部给其开了一个传递数据的入口,不管你数据哪里来的,都通过我这里传递进入,方式上就比较统一了,无论是全局store还是上次的props,都可以通过这里以props的方式统一注入,那么组件内部呢都给我通过props的方式访问外部传递进来的方法或者数据。

当然,上述关于UI组件的分类说明比较严格了,实际项目开发中也存在很多包含了业务代码以及自身state的组件使用connect链接store的做法。这里只是科普UI组件容器组件的概念,有时候硬要将一个组件抽离成两个组件反而是一件麻烦事,具体看大家习惯。

上面说了connect用于帮助组件链接数据,那么具体做呢?其实就得依赖上面提到的两个参数了,我们接着说。

贰 ❀ 叁 mapStateToProps

顾名思义,建立stateprops的映射,mapStateToProps接受2个参数,比如:

const mapStateToProps = (state,ownProps) => {
  return {
    name:state.name,
    age:ownProps.age
  }
}

说直白点,比如从全局的storestore本身可以理解为全局的state)以及外层组件传递给你的props中提取并组合成当前组件所需要的数据。

上面的代码中返回了一个新的对象,这个对象包含了一个name一个age,数据来源分别是全局state与外层组件传递的props,那么当前组件通过this.props即可访问到这两个属性,而且,无论是外部的state还是外部传递的ownProps发生了变化,都会再次触发此方法(如果你没用这两个参数就不会触发,毕竟没有依赖关系),目的是同步更新传递当前组件的props

贰 ❀ 肆 mapDispatchToProps

看到方法名中的dispatch,直觉应该想到此方法应该跟store.dispatch有关。没错,我们在学习context时,凡是组件需要派发action时最终调用的都是this.context.dispatch,写法上多少有些繁琐。mapDispatchToProps的作用,其实也是将dispatch行为抽离了出去,然后也作为props一部分传进了组件,此方法接受两个参数,如下:

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrement: () => {
      dispatch(Actions.increment(ownProps.caption));
    },
  }
}

dispatch作用其实与store.dispatch作用相同,只是react-redux对齐进行了封装,我们不用再借用this.context.dispatch调用,而是可以直接在mapDispatchToProps方法中使用,是不是用法上便捷了很多呢?其次,mapDispatchToProps返回了一个对象,包含了一个方法,那么在当前组件内部,调用this.props.onIncrement本质上其实就是在做派发。

ownPropsmapStateToProps的第二个参数作用相同,都是外层组件组件传递的props

叁 ❀ 一个例子

OK,我们介绍了react-redux带来的新概念,让我们使用这些API改写上一篇文章中的例子,近距离感受它们所带来的便捷性。在前面我们其实已经完成了对index.js的改写,接下来要做的是对Counter组件与Summary组件的改写,先上Counter.js的代码:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
import { connect } from 'react-redux'
class Counter extends Component {
  render() {
    const { onIncrement, onDecrement, caption, value } = this.props;
    return (
      <div>
        <button onClick={onIncrement}>+</button>
        <button onClick={onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired,
  value: PropTypes.number.isRequired
};
// 用于将`store`的数据加工成当前组件所需要的数据
function mapStateToProps(state, ownProps) {
  console.log(state);
  console.log(ownProps);
  return {
    value: state[ownProps.caption]
  }
}
// dispatch的操作都被提到这里了,组件瞬间就干净了
function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrement: () => {
      dispatch(Actions.increment(ownProps.caption));
    },
    onDecrement: () => {
      dispatch(Actions.decrement(ownProps.caption));
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

这里我们尝试打印mapStateToProps两个参数,你会发现其实state就是全局storeownProps就是父级组件传下来的props

你会发现,Counter组件活生生被抽离成了一个UI组件,以前我们还在组件中定义包裹dispatch的方法,定义初始化state的方法,还需要添加subscribe订阅store的变化,但现在,store的变化感知以及组件state的初始化都交给了mapStateToProps来完成,前面说了,只要外层state变化,都会触发此方法重新渲染。

同理,对于dispatch方法的定义我们也交给了mapDispatchToProps来完成,而组件内部只需要通过this.props.onIncrement就能执行action派发,是不是相对于之前的写法,简洁了很多呢?

同理,我们修改Summary组件,代码如下:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
class Summary extends Component {
  render() {
    const {sum} = this.props;
    return (
      <div>Total Count: {sum}</div>
    );
  }
}

Summary.propTypes = {
  sum: PropTypes.number.isRequired
};

function mapStateToProps(state) {
  let sum = 0;
  for (const key in state) {
    if (state.hasOwnProperty(key)) {
      sum += state[key];
    }
  }
  return {sum};
}

export default connect(mapStateToProps)(Summary);

保存代码,这个小例子又运行起来,相较于redux结合context的写法,react-redux确实很大程度上精简了代码量。

肆 ❀ 总

那么到这里,我们通过react-redux再次改写了之前的例子,通过这篇文章,我们知道react-redux与传统redux的差异性,从写代码的角度,不得不说react-redux更加的精简与方便。当然,redux其实还有不少进阶知识我们还未提及,比如中间件,比如异步处理,再或者对于reducer的合并等等,这些知识在后面的文章我们会慢慢介绍,那么本文到此结束。

参考

Redux 中文文档

Redux 入门教程(三):React-Redux 的用法

深入浅出React和Redux第三章

posted on 2021-07-04 23:40  听风是风  阅读(362)  评论(0编辑  收藏  举报