react-redux

414 阅读7分钟

前言

之前介绍了 redux 的基本用法和中间件,本文主要介绍 redux 在 react 中的使用。react-redux 库是 Redux 官方提供的 React 绑定库。 具有高效且灵活的特性。在平时的开发中,可以使用 react-redux 库,也可以不使用,但是如果使用此框架,就会如虎添翼。后面会有实例来分别展示使用与不使用的区别。

UI 组件 与 容器组件

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。

UI 组件

UI 组件的特征:

  • 只负责 UI 的呈现,不带有任何业务逻辑
  • 没有状态
  • 所有数据都由参数(this.props)提供
  • 不使用任何 Redux 的 API

UI 组件的例子

const Title = value => <h1>{value}</h1>;

容器组件

容器 组件的特征:

  • 负责管理数据和业务逻辑,不负责 UI 的呈现
  • 带有内部状态
  • 可以使用 react-redux 的 API

总之,UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

如果一个组件既有 UI 又有业务逻辑,那怎么办?回答是,将它拆分成下面的结构:外面是一个容器组件,里面包了一个UI 组件。前者负责与外部的通信,将数据传给后者,由后者渲染出视图。

React-Redux 规定,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它。

connect()

React-Redux 提供connect方法,用于从 UI 组件生成容器组件。顾名思义,connect 是一个桥梁,用来连接 UI 组件和容器组件。

connect 的核心是将开发者定义的组件,包装转换生成容器组件。所生成的容器组件能够使用 Redux store 中的哪些数据,全部由 connect 的参数来确定。

import { connect } from 'react-redux'
const VisibleTodoList = connect()(TodoList);

上面代码中,TodoList 是 UI 组件,VisibleTodoList 就是由 React-Redux 通过 connect 方法自动生成的容器组件

但是,因为没有定义业务逻辑,上面这个容器组件毫无意义,只是 UI 组件的一个单纯的包装层。为了定义业务逻辑,需要给出下面两方面的信息。

  • 输入逻辑:外部的数据(即state对象)如何转换为 UI 组件的参数

  • 输出逻辑:用户发出的动作如何通过 Action 对象,从 UI 组件传出去。

因此,connect方法的完整 API 如下。

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

上面代码中,connect 方法接受两个参数:mapStateToPropsmapDispatchToProps。它们定义了 UI 组件的业务逻辑。前者负责输入逻辑,后者负责输出逻辑,即将用户对 UI 组件的操作映射成 Action。

mapStateToProps()

mapStateToProps 是一个函数,它的作用是给返回的容器组件注入 props,props 来源于 Redux store 中的状态,所以这个函数一定要返回一个纯 JavaScript 对象。如下图所示:

const mapStateToProps = (state) => {
    return {
        likeNum: state.likeNum,
        hateNum: state.hateNum
    }
}

mapStateToProps 会订阅 Store,每当 state 更新的时候,就会自动执行,从而触发 UI 组件的重新渲染。

connect 方法可以省略 mapStateToProps 参数,那样的话,UI 组件就不会订阅Store,就是说 Store 的更新不会引起 UI 组件的更新。

mapDispatchToProps()

mapDispatchToProps 是 connect 函数的第二个参数,它的作用是将 dispatch 作为 props 传递给容器组件。也就是说,它定义了哪些用户的操作应该当作 Action 传给 Store。

export const likeEvent = () => ({
    type: 'like'
});

export const hateEvent = () => ({
    type: 'hate',
});

const mapDispatchToProps = {
    likeEvent,
    hateEvent
}

小结 mapStateToProps 和 mapDispatchToProps 定义了展示组件需要用到的 store 的内容,其中 mapStateToProps 负责输入逻辑,就是将状态数据映射到展示组件的参数(props)上。mapDispatchToProps 负责输出逻辑,即将用户对展示组件的操作映射成 action。

讲到这里,大家可能有一个疑问:connect 是如何获取到 Redux store 中的内容的?这就要借助于 Provider 组件来实现了。

Provider 组件

React-Redux 提供Provider组件,可以让最外层的容器组件拿到state。一般做法是需要将 Provider 作为整个应用的根组件,并获取 store 作为props,如下所示:

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

它的原理是 React 的 context 属性,store 放在了上下文对象 context 上面。然后,子组件就可以从 context 拿到store。

react 的 Context 属性

什么是 context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

在一个典型的 React 应用中,数据是通过 props 属性自上而下进行传递的,但这种做法在某些情况下比较繁琐的,比如某些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必通过组件树逐层传递 props。

创建 context

创建一个 context 对象

const { Provider, Consumer } = React.createContext();

在控制台中打印创造出来的创建的 context 对象,可以看到里面有两个关键的 api:

  • Provider(生产者):放置共享的数据
  • Consumer(消费者):Consumer 需要嵌套在 Provider 下面,才能通过回调的方式拿到共享的数据源

context 的简单应用

ContextDemo

import React from "react";
import Father from './compontents/context/father';

export const { Provider, Consumer } = React.createContext('default');

class ContextDemo extends React.Component{
    render(){
        let name = 'son';
        return (
            <Provider value={name}>
                <Father></Father>
            </Provider>
        )
    }
}

export default ContextDemo;

Father Page

import React from "react";
import Son from './son'

class Father extends React.Component{
    render(){
        return (
            <div>
                <div>This is Father Page</div>
                <Son />
            </div>
        )
    }
}

export default Father;

Son Page

import React from "react";
import { Consumer } from '../../context';

class Son extends React.Component{
    render(){
        return (
            <Consumer>
                {
                    (name) => {
                        return <div>{`my name is ${name}`}</div> 
                    }
                }
            </Consumer>
        )
    }
}

export default Son;

关于 React Context 有一篇讲的比较细致的文章 大家可以看下:www.jianshu.com/p/eba2b76b2…

不使用 react-redux 库的实例

以下是一个非常常见的场景,页面截图来自豆瓣 App,我们可以对影评 “点赞” 或者 “踩”,并且记录 “点赞” 和 “踩” 的数量。如下图所示:

入口

// redux 在 react 中的使用,不使用 react-redux 库
const render = () => {
  ReactDOM.render(
      <ReactAndReduxDemo
        value = {store.getState()}
        likeEvent = {() => store.dispatch({type: 'like'})}
        hateEvent = {() => store.dispatch({type: 'hate'})}
      />,
    document.getElementById('root')
  );
}
<!--需要通过 subscribe 来监听 store 的变化-->
store.subscribe(render);
<!--用于第一次触发-->
render();

UI 组件

import React from "react";
import './index.css'

class ReactAndReduxDemo extends React.Component{
    render(){
        const { value, likeEvent, hateEvent } = this.props;
        return (
            <div className="wrap">
                <div className="like" onClick={likeEvent}>点赞 <span>{value.likeNum}</span></div>
                <div className="hate"  onClick={hateEvent}><span>{value.hateNum}</span></div>
            </div>
        )
    }
}

export default ReactAndReduxDemo;

小结:

不借助于 react-redux 库的实现会有以下问题:

  1. react 与 redux 连接处数据的传递不够简洁和优雅
  2. store 与 组件耦合在一起
  3. 每次 store 发生改变都需要手动去监听

使用 react-redux 库的实例

仍然是上面的案例,使用 react-redux 库来实现

入口

import { reducer } from './compontents/reducer';
import { createStore } from 'redux';
import { Provider} from 'react-redux';
const store = createStore(reducer);

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

定义 action

export const likeEvent = () => ({
    type: 'like'
});

export const hateEvent = () => ({
    type: 'hate',
});

reducer 编写

// 初始状态
var initialState = {
    likeNum: 0,  // 点赞的总数
    hateNum: 0   // 踩的总数
}

export function reducer(state = initialState, action) {
    switch(action.type){
        case 'like':
            return {
                likeNum: state.likeNum + 1,
                hateNum: state.hateNum
            };
        case 'hate':
            return {
                likeNum: state.likeNum,
                hateNum: state.hateNum + 1
            };
        default:
            return state;
    }
}

页面展示

import React from "react";
import './index.css'
import { connect } from 'react-redux';
import { likeEvent, hateEvent } from '../action.js';

class ReactReduxDemo extends React.Component{
    render(){
        const { likeNum, hateNum, likeEvent, hateEvent } = this.props;
        return (
            <div className="wrap">
                <div className="like" onClick={likeEvent}>点赞 <span>{likeNum}</span></div>
                <div className="hate" onClick={hateEvent}><span>{hateNum}</span></div>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        likeNum: state.likeNum,
        hateNum: state.hateNum
    }
}

const mapDispatchToProps = {
    likeEvent,
    hateEvent
}

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