React@16.3 全新的Context API进阶教程

前言

最近看了下React16.3的新文档,发现官方悄悄地改了不少东西了。其中我最感兴趣的天然就是这个全新的Context API了。因此写了这篇文章来总结分享一下。其余的变更在这篇文章里或许会说起。vue

本文你能够在个人github上面找到,转载请标注这个地址就好了。node

什么是Context API

Context API是React提供的一种跨节点数据访问的方式。众所周知,React是单向数据流的,Vue里面的props也借鉴了这一思想。react

可是不少时候,这种单向数据流的设定却变得不是那么友好。咱们每每须要从更高层的节点获取一些数据,若是使用传统的prop传递数据,就须要每一层都手动地向下传递。对于层次很高的组件,这种方法十分地烦人,极大地下降了工做效率。git

因而,React使用了Context APIContext API存在已久,可是旧的Context API存在不少问题,而且使用起来也并非特别方便,官方并不建议使用老版本的Context API。因而不少开发者选择了Redux之类的状态管理工具。程序员

受到Redux的影响,React在16.3.0版本中推出了全新的Context APIgithub

一些你须要提早知道的东西

  1. 众所周知,长期起来JavaScript一直没有模块系统。nodejs使用require做为弥补方法。ECMAScript6以后,引入了全新的import语法标准。import语法标准有个尤其重要的不一样(相比较require),那就是:import导入的数据是引用的。这意味着多个文件导入同一个数据,并非导入的拷贝,而是导入的引用。数组

  2. react@16.3的声明文件(d.ts)貌似没有更新,意味着若是你如今使用Typescript,那么可能会报错。markdown

  3. React如今推荐使用render propsrender props为组件渲染的代码复用以及代码传递提供了新的思路,其实本质上就是经过props传递HOC函数来控制组件的渲染。app

  4. 或许你曾经听过“Context API是用来替代Redux”之类的传闻,然而事实并不是如此。ReduxContext API解决的问题并不同,会形成那样的错觉多是由于他们的使用方法有点儿同样。dom

  5. React16.3有几个新特性,最主要的变化是Context,还有就是废除了几个生命周期,好比ComponentWillReceiveProps(说实话,实际项目中,这个生命周期彻底能够用ComponentWillUpdate来替换)

  6. React16.3中的refs再也不推荐直接传递一个函数了,而是使用了全新的React.createRef来替代。固然之前的方法依旧适用,毕竟是为了兼容。

开始使用

React.createContext

createContext用来建立一个Context,它接受一个参数,这个参数会做为Context传递的默认值。须要注意的是,若是你传入的参数是个对象,那么当你更改Context的时候,内部会调用Object.is来比较对象是否相等。这会致使一些性能上的问题。固然,这并不重要,由于大部分状况下,这点儿性能损失能够忽略。

咱们看下这个例子,这是一个提供主题(Light/Dark)类型的Context

// context.js
import * as React from 'react';
// 默认主题是Light
export const { Provider, Consumer } = React.createContext("Light");

复制代码

接下来咱们只须要在须要的文件里import就好了

Provider

Provider是须要使用Context的全部组件的根组件。它接受一个value做为props,它表示Context传递的值,它会修改你在建立Context时候设定的默认值。

import { Provider } from './context';
import * as React from 'react';
import { render } from 'react-dom';
import App from './app';


const root = (
    <Provider value='Dark'> <App /> </Provider>
);

render(root, document.getElementById('root'));


复制代码

Consumer

Consumer表示消费者,它接受一个render props做为惟一的children。其实就是一个函数,这个函数会接收到Context传递的数据做为参数,而且须要返回一个组件。

// app.jsx

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

export default class App extends React.Component {
    render() {
        return (
            <Consumer> { theme => <div>Now, the theme is { theme }</div> } </Consumer>
        )
    }
}
复制代码

一些须要注意的地方

多层嵌套

Context为了确保从新渲染的快速性,React须要保证每一个Consumer都是独立的节点。

const ThemeContext = React.createContext('light');
const UserContext = React.createContext();

function Toolbar(props) {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Toolbar />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}
复制代码

当层次更加复杂的时候,会变得很烦人。所以推荐当层次超过两层以后,建立一个本身的render prop或许是个不错的主意。在实际工程中,其实并不建议多层嵌套。更为适合的时,提供一对ProvierConsumer对,传递状态管理工具对应的实例就好了。

在生命周期中使用

在以前的Context API中,在一些声明周期中会暴露一个context的参数,以供开发者更为方便的访问。新版API并无这个参数传递了,更为推荐的方式是直接把Context的值经过props传递给组件。具体来讲,就像下面这个官方的例子这样。

class Button extends React.Component {
  componentDidMount() {
    // ThemeContext value is this.props.theme
  }

  componentDidUpdate(prevProps, prevState) {
    // Previous ThemeContext value is prevProps.theme
    // New ThemeContext value is this.props.theme
  }

  render() {
    const {theme, children} = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}> {children} </button>
    );
  }
}

export default props => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} />}
  </ThemeContext.Consumer>
);
复制代码

不像之前那样,能够直接经过this.context访问,新版本的Context只能在render方法里面访问。由于Context只暴露在Consumerrender prop里面。我的以为这是这个版本API的一个缺点。因此只有采用上面这种折中的方式,再包装一个函数组件来封装到props里面去。相比较而言,仍是麻烦了一点儿。在组件树里面多了一个函数组件,也是一个缺点。

Consumer封装

当一个Context的值多个组件都在使用的时候,你须要手动地每次都写一次Consumerredner prop。这是很烦的,程序员都是很懒的(至少我是这样),所以这个时候利用一下React的HOC来封装一下来简化这个过程。

const ThemeContext = React.createContext('light');

function ThemedButton(props) {
  return (
    <ThemeContext.Consumer>
      {theme => <button className={theme} {...props} />}
    </ThemeContext.Consumer>
  );
}
复制代码

接下来,当你须要使用Context的时候,就不须要在写什么Consumer

export default props => (
    ThemeButton(props)
);
复制代码

转发refs

当你封装完一个Consumer以后,或许你想要用ref来获取Consumer里面根组件的实例或者对应的DOM。若是直接在Consumer上使用ref,是得不到想要的结果的。因而在React16.3里面,使用了一种全新的技术(不肯定是否是16.3才引入的),叫作转发refs 。不只仅用在Context里面,实际上,在任何你想要把ref传递给组件内部的子组件的时候,你均可以使用转发refs

具体来讲,你须要使用一个新的API:React.forwardRef((props, ref) => React.ReactElement),如下面这个为例:

class FancyButton extends React.Component {}

// Use context to pass the current "theme" to FancyButton.
// Use forwardRef to pass refs to FancyButton as well.
export default React.forwardRef((props, ref) => (
  <ThemeContext.Consumer>
      {
        theme => (
            <FancyButton {...props} theme={theme} ref={ref} />
        )
      }
  </ThemeContext.Consumer>
));
复制代码

React.forwardRef()接受一个函数做为参数。实际上,你能够将这个函数当作一个函数组件,它的第一个参数和函数组件同样。不一样的地方在于,它多了一个ref。这意味着若是你在React.forwardRef建立的组件上使用ref的话,它并不会直接被组件消化掉,而是向内部进行了转发,让须要消化它的组件去消化。

若是你以为难以理解,其实这种方法彻底能够用另外一种方法替代。咱们知道,在React中,ref并不会出如今props中,它被特殊对待。可是换个名字不就好了吗。

须要提一下的是,之前咱们获取ref是传递一个函数(不推荐使用字符串,这是一个历史遗留的问题,ref会在某些状况下没法获取到正确的值。vuejs可使用,不要搞混了)。可是这个过程很烦的,咱们只须要把实例或者DOM赋值给对应的变量就好了,每次都写一下这个同样模板的代码,很烦人的好吗。“千呼万唤”中,React终于听到了。如今只须要React.createRef就能够简化这个过程了。

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    render() {
        return <div ref={this.myRef} />; } } 复制代码

使用方法就这么简单,没什么特别的地方。

回到上面的话题,如今咱们用props来实现转发refs的功能。

class Input extends React.Component {

    reder() {
		return (
			<label>Autofocus Input:</label>
			<input ref={this.props.forwardRef} type="text" />
		)
    }

}

function forwardRef(Component, ref) {
	return (<Component forwardRef={ref} />); } // 使用forwardRef let input = React.createRef(); forwardRef(Input, input); // 当组件绑定成功以后 input.current.focus(); 复制代码

React.createRef返回的值中,current属性表示的就是对应的DOM或者组件实例。forwardRef并无什么特殊的含义,就是一个简单的props。这个用法就像是状态提高同样。