React状态管理之Context

抛出问题

在平时使用react的过程中,数据都是自顶而下的传递方式,例如,如果在顶层组件的state存储了theme主题相关的数据作为整个App的主题管理。那么在不借助任何第三方的状态管理框架的情况下,想要在子组件里获取theme数据,就必须的一层层传递下去,即使两者之间的组件根本不需要该数据;就如同下图所示,并且如果App的层级越深,这之间的层层传递对开发者来说可谓是灾难。

react数据流.png

引入context

正因为有了跨越层级传递值的这么一种需求,其实官方也提供了context的机制。通过context,我们就能够在子组件里获取祖先组件里的值,而不需要层层传递。其实很多的状态管理框架与react结合的库就是使用了context特性,例如著名的react-redux。

在v16.3之前的context只是官方的实验性API,其实官方是不推荐开发者使用的,但是架不住很多框架依旧在使用它;所以官方在v16.3发布的新的context API,新的API会更加的易用,本文也是以v16.3为准。

在新的context API中,React提供了一个createContext的方法,该方法返回一个包含了Provider,Consumer的对象,而Provoider,Consumer对象就是新API的重点。

我们先看一个简单的例子,再来讲解API。在本案例中,在顶层父组件的state存储着控制这个App的theme的一些属性,使用context来跨组件传递这些属性,使得底层组件能够直接得到这些属性。

首先在themeContext.js文件中定义context,并导出Provider以及Consumer:

import {createContext} from "react";

export const {Provider, Consumer} = createContext({
  color: "green",
  fontSize: "20px"
});

createContext需要传递一个参数,叫做defaultValue。这个值会在什么时候起作用呢?这个稍后解释。

然后我们就可以直接在顶层的App组件中,直接使用Provider:

import React, {Component} from 'react';
import {Provider} from "./context/themeContext";
import Parent from "./Parent";

class App extends Component {
  state = {
    color: "red",
    fontSize: "16px"
  };

  render() {
    return (
      <div className="App">
        <Provider value={this.state}>
          <Parent/>
        </Provider>
      </div>
    );
  }
}

export default App;

我们直接在顶层的组件里使用Provider组件,并且Provider组件有一个value属性用于传递context的实际的value。然后我们就可以在底层的Child组件中得到这些value来使用。

层级关系:App -> Parent -> Child

import React, {PureComponent} from "react";
import {Consumer} from "./context/themeContext";

class Child extends PureComponent {
  render() {
    return <Consumer>
      {
        style => <div style={style}>This is Child Component that gets style value through context.</div>
      }
    </Consumer>
  }
}

export default Child;

在Child组件中使用Consumer,就能够得到上层所传递context的值;Consumer的需要一个函数作为子元素,该函数的参数就是上层所传递context value,然后就可以返回该组件具体的组件样式了。

这就是一个简单的使用context的例子,可以看到context的API是非常简单的,也可容易使用,再简单总结一下API:

  • createContext:用于创建context,需要一个defaultValue的参数,并返回一个包含Provider,以及Consumer的对象
  • Provider:顶层用于提供context的组件,包含一个value的props,value是实际的context数据
  • Consumer:底层用于获取context的组件,需要一个函数作为其子元素,该函数包含一个value的参数,该函数的参数就是上层所传递context value

看到这里,你可能会有一个疑惑:为什么createContext需要一个defaultValue,而Provider还需要一个实际的value?到底defaultValue是什么时候起作用呢?先抛出结论:只有在上层组件没有提供Provider组件时,下层组件的Consumer才会直接使用defaultValue作为子函数的参数传递。以本例子为例,只有在App组件压根没有使用Provider组件时,Child组件中的Consumer的子函数参数才会是{ color: "red", fontSize: "16px" }这个defaultValue,其他情况都不会使用到这个值。这个地方有一个常见的误解:就是不给上层组件的Provider的value属性,或者让value={undefined}时,就会使用defaultValue,这是不对的!!!请切记,大家也可以自己尝试,看看是不是这个结论。

更近一步

虽然使用了Consumer能够让我们很方便的得到context的value,但是如果很多子元素要得到context的值,都去先调用Consumer,再在它的子函数里返回真正的组件内容,会显得十分的累赘。所以我们可以对Consumer进行一个简单的封装,封装一个connect的方法。去实现类似于react-redux其中的connect函数的效果。connect方法的代码如下:

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

export default mapState => {
  return WrappedComponent => {
    const Component = props => (<Consumer>
      {
        value => {
          let mappedProps = mapState(value);
          return <WrappedComponent {...props} {...mappedProps}/>
        }
      }
    </Consumer>);
    Component.displayName = `connect(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
    return Component;
  }
};

简单解释一下:connect方法需要传入一个mapState方法,mapState方法是context的value映射方法,当调用connect方法后,会依旧返回一个函数;该函数实际是一个高阶函数工厂,将传入的WrappedComponent组件用Consumer包裹里面,并结合之前的mapState映射得到具体的计算后的props属性,并把这些props属性都赋予给WrappedComponent。这样,我们在之后想要得到context时,只需要简单调用一下该方法即可。

再结合一个例子看看怎么使用connect方法:假如现在有一个App用户显示学生的相关信息;学生的信息包含了name,age,gender三个属性;此外有两个组件Student、StudentGender;Student用于显示学生的name,age,并且有一个+按钮,点击就会在当前年龄加一岁。

层级关系如下:App -> StudentContainer -> Student

App组件的代码如下:

import React, {Component} from 'react';
import {Provider} from "./context";
import StudentContainer from "./StudentContainer";

class App extends Component {

  onIncreaseAge = () => {
    this.setState(preState => ({
      age: preState.age + 1
    }))
  };

  state = {
    name: "张三",
    age: 12,
    gender: "男",
    onIncreaseAge: this.onIncreaseAge
  };

  render() {
    return (
      <div className="App">
        <Provider value={this.state}>
          <StudentContainer/>
        </Provider>
      </div>
    );
  }
}

export default App;

在App组件中,我们将student的属性以及增加年龄的方法一同传递给了context,使得子组件既能获得属性,也能调用修改属性的方法。

Student组件的代码如下:

import React from "react";
import {connect} from "./context";

const Student = ({studentName, studentAge, onIncreaseAge}) => {
  return <div>
    <span className="title">Student:</span>
    <ul>
      <li>name: {studentName}</li>
      <li>age: {studentAge}
        <button onClick={onIncreaseAge}>+</button>
      </li>
    </ul>
  </div>;
};

const mapState = state => ({
  studentName: state.name,
  studentAge: state.age,
  onIncreaseAge: state.onIncreaseAge
});
export default connect(mapState)(Student);

可以看到,当我们使用了connect方法后,Student组件就变成了一个傻瓜组件,只需要专心负责显示数据即可。

结语

以上就是关于context的简单介绍,可以看到它确实十分简单的实现了跨层级传递数据的功能。所以当我们想要跨层级传递数据时,而数据本身要传递的地方不多,这个时候往往不想再引入一个更复杂的状态管理框架(如redux等),这个时候,context会是一个十分不错的选择。
本文所涉及到的案例的地址在此,其中第一个案例在分支sample-theme中,第二个案例在分支encapsulate中。

如果对本文有什么意见和建议,欢迎讨论和指正!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容