I Want To Know React - Context 语法

回顾 Context

上一章节中,我们介绍了何谓 context。

Context 是一种利用向下广播来传递资料的方式,此方法可以解决 props 必须要一层层向下传递的缺点。

而根据 React 官方推荐,我们应该只在要将全域性资料(e.g. 使用者资讯、时区设定、语系、UI 主题 ...etc)向下传递给很多底层 component 时才应该使用 context。

如果是要避免中间层 component 传递过多细节 props 的话,开发者可以使用 composition 的技巧,把整个下层 component 作为 props 传递下去即可。

在本章节中,我们将介绍 context 的详细语法。

Context 使用概念

在开始前,先让我们来概述一下使用 context 的概念。

Context 角色

React context 的使用会环绕三个角色在运作:

  • Context Object
  • Provider
  • Consumer

一个 React app 中可以有多个 React context。每个 React context 的本体都是一个物件(在这边把它称为 context object)。其中 context object 中又会有两个很重要的属性:Provider(提供者)与 Consumer(消费者)。

  • Provider(提供者)的功用就是用来提供 context 值。
  • Consumer(消费者)的功用则是用来使用 context 值。

使用 Provider 的 component 与使用 Consumer 的 component 之间不需要是直接的父子层关系。Provider 只要在 Consumer 的上层即可让 Consumer 接收到 context 值,而处於 Provider 与 Consumer 之间的中间层 component 则不须做任何的改动。

Context 使用步骤

根据以上的资讯,我们可以把使用 Context 归纳成以下几个步骤:

  1. 创建 React context object
  2. 在 Provider 中放入值,以将该值广播给自己以下的 Consumer component 使用
  3. Consumer 接收值,可根据接收到的值 component 可显示对应的内容或执行对应的动作

接下来,就让我们进入介绍语法的篇章吧!

React.createContext

首先要先学习如何创建一个 React context。

创建 React context 需使用 React.createContext 这个函式。

更详细一点说明,这个函式有两个功能:

  • 创建 context object(或说 context type)
  • 设定 context 的预设值

语法如下所示:

const MyContext = React.createContext(defaultValue);

React.createContext 需要带入一个参数:

  • defaultValue:代表这个 context 的预设值,与 props 一样,可为任意的值

    因为代表预设值,所以只有在 Consumer 以上的 component 中都没有 Provider 时才会使用到 defaultValue 的内容。

    需要注意的是,如果 Consumer 上面有 Provider,但此 Provider 的值为 undefined 的话,则 Consumer 依然不会使用 defaultValue,拿到的值会是 undefined

React.createContext 会回传一个值:

  • Context object:也可以说是 context type,会是一个 JavaScript object

    在下一个段落中会更详细的介绍 context object。

Context Object

接着要介绍 Context object。

如刚刚所讲,Context object(Context type)会是一个 JavaScript object。每个 Context object 中会有两个很重要的属性:

  • Provider
  • Consumer

把 context object 的 log 下来的话,即可看到这两个属性:

console.log(MyContext);
// {$$typeof: Symbol(react.context), Consumer: {$$typeof: Symbol(react.context), _context: {…}, …}, Provider: {$$typeof: Symbol(react.provider), _context: {…}}, _calculateChangedBits: null, _currentValue: 123, _currentValue2: 123, _threadCount: 0, …}

读者也可以到 CodePen 上的 console 查看内容。

ProviderConsumer 的详细内容将在下面介绍。

Context.Provider

每个 Context 物件中都会有 Provider 属性,其用途就是将指定的值传给更下层的 Consumer 使用。

Context.Provider 是一个 React element,因此可以使用 JSX 表示。语法如下:

<MyContext.Provider value={/* some value */}>{/* ... */}</MyContext.Provider>

Context.Provider 可以带入一个 prop:

  • value:代表提供给 Provider 以下的 Consumer 使用的值,与 props 一样,可为任意的值。

    换句话说,Provider 提供了 value 後,更下层的 component 都可以用 Context.Consumer 接收 value 的内容。

可以巢状使用 Context.Provider

Context.Provider 中可以再包裹 Context.Provider

当巢状使用同一个 Provider 时,内层的 Provider 会遮蔽掉(覆盖,而非 Merge)外层 Provider 的 value

也就是说内层Provider 以下的 component 会拿到内层的 value;介於内外层 Provider 之间的 component 则还是会拿到外层 Provider 的 value

读者可以查看此 CodePen 范例。

Context.Provider 的 value 改变时,底下的 consumer 必定会 re-render

需要注意的是,一旦 Context.Providervalue 改变,则所有使用此 Provider 以下的 Customer component 都会 re-render。

此 re-render 不会因为 shouldComponentUpdate 为 false 而取消,也不会因为 Consumer 以上的中间层的 component 没有更新而不执行。也就是这些 Consumer component "必定" 会 re-render。

因此在使用 Context.Provider 时需要注意不要 inline 赋值给 value,否则当使用 Provider 的 component re-render 时,使用此 Provider 的 Consumer 都会一并 re-render。这将造成极大的效能问题。

Value 改变与否是用 Object.is 来判断

至於 Context.Providervalue 改变与否则是使用 Object.is 来判断。

Object.is 基本上就是 === 的概念,只是额外增加了 NaN±0 的判断而已。

Object.is 的详情资讯请参考 MDN 的介绍

Context.Consumer

每个 Context 物件中都会有 Consumer 属性,其用途就是接收上层 Provider 传下来的值。

Context.Consumer 是一个 React element,因此可以使用 JSX 表示。语法如下:

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>
// {$$typeof: Symbol(react.element), type: {…}, key: null, ref: null, props: {…}, …}

Context.Consumer 可以带入一个 prop:

  • children:代表要 render 的内容,会是一个 function

    需要注意的是,children 是一个 function 而非 React element,如果 children 不是带入 function 的话,React 就会报警告。

    使用 function 当作 children 是 Render props 的技巧,此技巧将在之後章节介绍。

    children function 接受一个参数:

    • value:代表 Consumer 接收到的值,与 props 一样,可能为任意的值。

    children function 会回传一个值:

    • React element:即代表要 render 出来显示在画面上的内容

Consumer 会接收最靠近自己的 Provider 的值

Consumer 接收到的值会是由最靠近自己的 Provider 所提供的。

也就是,如果有巢状的 Provider 时,Consumer 拿到的值会是靠最内层 Provider 所提供的,较外层 Provider 的值会被遮蔽掉。

举例来说:

const MyContext = React.createContext({ theme: "default" });

const consumer = (
  <MyContext.Provider value="outerProvider">
    <MyContext.Provider value="innerProvider">
      <MyContext.Consumer>{(value) => <div>{value}</div>}</MyContext.Consumer>
    </MyContext.Provider>
  </MyContext.Provider>
);

ReactDOM.render(consumer, document.getElementById("root"));

范例中,有两个 Provider 包裹一个 Consumer。Consumer 拿到的会是最靠近自己的 Provider 所提供的值 "innerProvider",而非外层 Provider 提供的 "outerProvider"

如果读者想要自己试试的话可以参考这个 CodePen 范例。

Class.contextType

React class component 可以接受名为 contextType 的属性。此属性的用处与 Context.Consumer 相同,都是用来接收上层 Provider 传下来的值。

但与 Context.Consumer 不同,使用 Class.contextType 会分为以下两个步骤:

  1. 设定要使用的 context object
  2. 使用 context 内容

设定 contextType 语法

因为 class 本身并不会知道开发者要使用的 context 为何,因此要先指定 context object。

设定要使用的 context 的语法如下:

class MyClass extends React.Component {
  // ...
}
MyClass.contextType = MyContext;

如果专案有支援实验性 public class fields syntax 语法的话,也可使用 static 设定 contextType,两种语法效果相同:

class MyClass extends React.Component {
  static contextType = MyContext;
  // ...
}

因为是设定 context,因此 contextType 只能接受以下型别:

  • Context object:即代表要使用的 context

    如果 contextType 的内容不是 context object 的话,则 React 会报警告。

    MyClass.contextType = 123;
    // Warning: TestContextType defines an invalid contextType. contextType should point to the Context object returned by React.createContext().
    

取用 context 内容语法

接着来到取用 context 内容的部分。

如果要使用 contextType 方式取用 context 内容的话,则需要在 class 内使用 this.context

class MyClass extends React.Component {
  // ...
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

this.context 的内容与 Context.Consumer 接收到的值相同,都代表 Provider 传下来的 value

另外,this.context 的特性与 Context.Consumer 相同,都是接收最靠近自己的 Provider 的值。

this.context 可以在 lifecycle 函式中取用

另外一点与 Context.Consumer 不同的地方是,this.context 可以在所有 lifecycle 函式中取用,而不只限制在 render 函式中使用而已。

也就是说,commit 阶段的 lifecycle 函式(e.g. componentDidMountcomponentDidUpdate ...etc)就可以取用 this.context 的值来执行各种 side-effect:

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;

Context.Consumer vs. Class.contextType

如上面段落所提及,Class.contextTypeContext.Consumer 功能相同,都是用来取用最靠近自己的 context 值,然而它们之间还是有些区别,如下所示:

从可以使用的 component 种类来看

  • Context.Consumer 可以在 class component 与 function component 中使用
  • Class.contextType 则只能在 class component 中使用

从可以取用 context 的位置来看

  • Context.Consumer 因为是 React element,所以只能在 render 函式中使用
  • Class.contextType 则可以在各种 lifecycle 函式中使用,因此可以支援取用 context 值执行 side-effect

从可以使用的 context 数量来看

  • Context.Consumer 外还可以再包其他的 Consumer,因此一个 component 可以使用多种 context object
  • Class.contextType 因为语法的限制,一个 class component 只能指定使用一种 context object

Context.displayName

最後,context object 可以支援使用 displayName 属性,可让 React dev tool 上的 context 显示为 displayName 设定的名称:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider /> // "MyDisplayName.Provider" in React DevTools
<MyContext.Consumer /> // "MyDisplayName.Consumer" in React DevTools

因为是设定名称,displayName 只能接受以下型别:

  • String:代表 React dev tool 上 context 的名称

预设的 context displayName 就是 "Context"

如果不设定 displayName 的话,context 预设的 displayName 就是 "Context"。

在有多个 context object 的状况下,如果都没设定 displayName 就会很难在 React dev tool 上区别各个 context,如下所示:

const MyContext = React.createContext(/* some value */);
const MyOtherContext = React.createContext(/* some value */);

<MyContext.Provider /> // "Context.Provider" in React DevTools
<MyContext.Consumer /> // "Context.Consumer" in React DevTools
<MyOtherContext.Provider /> // "Context.Provider" in React DevTools
<MyOtherContext.Consumer /> // "Context.Consumer" in React DevTools

因此建议读者在使用 context 时都尽量设定 displayName

小结

本章节介绍了 React context 的用法。

React context 的使用会环绕三个角色在运作:

  • Context object:代表 context 本身
  • Provider:用来提供 context 值
  • Consumer:用来使用 context 值

使用 Provider 的 component 与使用 Consumer 的 component 之间不需要是直接的父子层关系,只要 Provider 在 Consumer 的上层即可让 Consumer 接收到 context 值。

另外,我们也介绍了以下几个 context 语法:

  • React.creatContext
  • Context.Provider
  • Context.Consumer
  • Class.contextType
  • Context.displayName

在下一章中,我们将介绍一些 context 的范例以及特殊使用技巧。

参考资料


<<:  第29篇:结合

>>:  Day 27 Filebeat with multiple module and ELK Dashboard

[Day 3] 排版布局Container

网页的开始 於布局排版 现在的年代 也需要RWD适合部分版型 所以我们就由布局开始吧 常常会看到一种...

Day10 Collectionview小实作4

紧接着昨天~ 我们写了一个func 并且利用结构加入阵列的方式写入每个变数的字串以及图片。 而後在生...

Day-25: Ruby 世界好多等於,系虾米毁?

今天来说明一下,在Ruby的世界里,运算符代表什麽意思? 之前偶然间在等候区,和同学们讨论这个问题,...

DAY23 - 我的网站要分析!网站分析工具的选择和态度(1)

首先要做这个议题其实也是来自专案的过程的经验。以前听到客户要我们埋GA并提供给我们一段追踪码,就也搞...

110/11 - 把照片储存在Pictures/应用程序名称资料夹 - 1

不太可能每个专案都那麽爽,可以把相片储存在内部储存空间/Android/data/packageNa...