React 16.3新的Context API真的那么好吗?

React 16.3新的Context API真的那么好吗?

React v16.3还没有正式发布,但是已经预告了众多新功能,其中很引人注意的是新的Context API,今天就来聊一聊这个。

关于这个新的Context API有很多表扬的声音,但是我们可以和老的Context API做一个对比,发现可能并没有想象的那么好。

首先,React一直是有Context API的(我们姑且称为“老的Context API”),只是React团队自己都不建议使用,如果你去看官方网站的介绍,可以看见Facebook对“老的Context API”的忠告:

If you want your application to be stable, don’t use context.
If you aren’t familiar with state management libraries like Redux or MobX, don’t use context.
If you’re still learning React, don’t use context.

如果你想要稳定应用,不要用Context。

如果你并不熟悉Redux或者Mobx这样的状态管理框架,不要用Context。

如果才刚刚学React,不要用Context。

这就是Facebook给的建议,总之,能不用Context就不要用Context,Facebook明确说这个Context API只是“试验性”的,不要用!

这里真要加一个黑人问号,到底老的Context API有啥问题呢?因为单独看老的Context API没有比较,也看不出来个之乎所以然,所以还是要和新的Context API对比着看。

我们还是先来看新的Context API是怎样吧,因为React v16.3还没有正式发布,所以不方便直接来玩一玩,但是有人已经做了一个polyfill叫create-react-context,行为和新的Context API一致,可以直接拿来试一试。

首先,安装create-react-context,然后代码中就可以这么写。

新版Context API用法

import createReactContext from 'create-react-context';

等到React v16.3正式发布之后,只需要改这一行,其他代码不用碰,可以猜测上面这行代码只需要改成下面这样。

import {createReactContext} from 'react';

被导入的createReactContext,从名字的形式就知道是一个函数,用于创造一个Context对象,然后这个Context对象又包含两个属性,一个叫Provider,另一个叫Consumer,这两个属性都是纯种的React组件。

在组件树中,Provider负责提供context,而Consumer用来消费Provider提供的context,而且,它们之间可以隔着任意层级,依然保留心有灵犀,这就是Context的意义。

想象一下,如果没有Context,如果把顶层组件的数据隔若干层传给底层组件?

要么用prop,一层一层地传,要求每一层都要负责传递prop,丢了一个就完蛋了,这种方法当然是十分笨拙的,不可取。

要不然,就要利用一个全局性的对象,比如Redux中的Store,那就面临如何管理好全局资源的问题,

左右为难啊!

所以,最理想的方式是有一个——Context。

Context的一个典型应用场景是界面中的“主题”(Theme),包括颜色样式等内容,主题的设定放在顶层的组件中,在这个顶层组件之下的所有React组件都应该能够很方便地访问主题,接下来就让我们用“新的Context API”来解决这个问题。

简单一点,我们只创造两个主题:defaultTheme和fooTheme。

const defaultTheme = {
  background: 'white',
  color: 'black',
};
const fooTheme = {
  background: 'red',
  color: 'green',
}
const ThemeContext = createReactContext(defaultTheme);

有了ThemeContext,然后就可以使用ThemeContext.Provider和ThemeContext.Consumer,这两者因为都源于同一个ThemeContext对象,所以数据可以关联起来。

首先我们看怎么消费context。

const Banner = ({theme}) => {
  return (<div style={theme}>Welcome!</div>);
};

const Content = () => (
  <ThemeContext.Consumer>
    {
      context => {
        return <Banner theme={context} />
      }
    }
  </ThemeContext.Consumer>
);

注意,ThemeContext.Consumer使用的是render props这种模式,render props模式指的是让prop可以是一个render函数。

(在我写《深入浅出React和Redux》的时候,对这种模式还没有明确说法,书中我称这种为child-as-a-function模式,不过,现在业界普遍都认可这种模式叫render props。)

ThemeContext.Consumer的子组件是一个函数,要知道子组件可以认为是this.props.children,所以也属于render props模式。

在上面的例子中,子组件就是下面的render函数。

    context => {
       return <Banner theme={context} />
     }

这个函数被调用的是偶,参数就是context,至于如何使用这个context,完全由这个函数来操纵,因为这个函数中可以包含任意代码,这种模式拥有相当大的自由度,到底使用context上哪些数据,如何使用这些数据,完全可以由code来定制。

接下来,看如何提供context,我们创造一个通过点击按钮切换主题的ThemeProvider。

class ThemeProvider extends React.Component {
  state = {
    theme: defaultTheme
  }

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        <Content/>
        <div>
          <button onClick={() => {
            this.setState(state => ({
              theme: state.theme === defaultTheme ? fooTheme : defaultTheme
            }))
          }}>
            Toggle Theme
          </button>
        </div>
      </ThemeContext.Provider>
    );
  }
}

当Toggle Theme按钮被点击的时候,ThemeProvider的state被改变,state的改变引起ThemeProvider的重新渲染,重新渲染引起render函数被调用,从而引起ThemeContext.Provider的重新渲染,传递给value属性的是最新的state,这样,就把新的context值应用上了。

点击Toggle Theme按钮,Banner的色调会来回切换。

老的Context API用法

接下来,我们看老的Context API如何来做。

在老的Context API中,没有所谓“Context对象”,无论消费还是提供context,都由React组件自己搞定。

先说消费者角度,一个组件如果要访问context,要通过contextTypes声明自己要访问什么样的context,然后,就可以通过this.context访问对应的context,对于本身就是一个函数的React组件,没有this的概念,通过函数的第二个参数访问context,像下面这样。

const Banner = ({}, context) => {
  return (<div style={context.theme}>Welcome!</div>);
};

Banner.contextTypes = {
  theme: PropTypes.object
};

const Content = () => (
  <Banner />
);

嘿,当这套“试验性”老的Context API被创造的时候,PropTypes还是React核心库的一部分,现在PropTypes已经独立出来了,从这个意义上说,这套API看起来真有点过时了。

从context提供角度来看,同样,需要提供者明确声明自己要提供什么样的context。

同样是ThemeProvider,利用childContextType声明context长得什么样子,通过getChildContext来提供context的实际值。

class ThemeProvider extends React.Component {
  state = {
    theme: defaultTheme
  }

  getChildContext() {
    return this.state;
  }

  render() {
    return (
      <div>
        <Content/>
        <div>
          <button onClick={() => {
            this.setState(state => {
              return {
                theme: state.theme === defaultTheme ? fooTheme : defaultTheme
              };
            })
          }}>
            Toggle Theme
          </button>
        </div>
      </div>
    );
  }
}

ThemeProvider.childContextTypes = {
  theme: PropTypes.object
};

每当ThemeProvider被渲染的时候,它的getChildContext就会被调用,返回值就是它所提供的context。

新老两代Context API的比较

好了,现在体会到新老Context API如何来用,可以开始吐槽了。

新的Context API获得广泛赞誉,我在社交网络上浏览了这些赞誉之词,发现无外乎两个原因:

  1. 应用了render props模式,感觉好牛逼;
  2. React宣称迁移到新的Context API之后,就会摘掉“试验性”的帽子,感觉好牛逼。

虽然感觉好牛逼,但是并不表示没有问题。

新的Context API通过创建一个Context对象来完成,在实际项目中,Provider和Consumer往往都存在于不同的源代码文件中,如何让他们访问同一个Context对象呢?

一个最直接的方式,是在一个文件中定义Context对象,然后,这个文件被被其他文件来import。

可是,使用老的Context API并不需要这样啊!

老的Context API虽然老,但是Provider和Consumer之间不需要共同依赖一个什么对象来工作,只要都依赖React(这是当然)就足够了。

新的Context API貌似用render props这种模式优雅地解决了context的问题,但是却引入新的问题——如何管理context对象。

对于只有一个Context对象的场景,还比较好处理,但是,加入需要多个同种类的Context,那就麻烦了。比如,对于一个列表应用,列表中可以有任意多个列表项组件,如果要求每个列表项都要有自己的Context,那怎么让列表项之下的组件找到所属的Context呢?

我脑子里已经想出了一个解决方法,但是还是感觉麻烦,下次再来介绍,大家也可以想一想自己的解法。

新的Context有一个明显实用好处,可以很清晰地让多个Context交叉使用,比如组件树上可以有两个Context Provider,而Consumer总是可以区分开哪个context是哪个。

<Context1.Consumer>
  {
     context1 => (
       <Context2.Consumer>
         {
           context2 => {
              // 在这里可以通过代码选择从context1或者context2中获取数据
              return ...;
           }
         }
       </Context2.Consumer> 
     )
  }
</Context1.Consumer>

总结

总之,我个人的观点就是:不要被对新事物的鼓吹冲昏头脑,对于新东西,不光要看到它的好处,也要看到它带来的麻烦。

编辑于 2018-03-09

文章被以下专栏收录