重拾React: Context

前言

  首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励,但愿你们多多关注呀!很久已经没写React,发现连Context都发生了变化,突然有一种村里刚通上的网的感受,可能文章所说起的知识点已经算是过期了,仅仅算做是本身的学习体验吧,javascript

Context

  对于React开发者而言,Context应该是一个不陌生的概念,可是在16.3以前,React官方一直不推荐使用,并声称该特性属于实验性质的API,可能会从以后的版本中移除。可是在实践中很是多的第三方库都基于该特性,例如:react-redux、mobx-react。java

  如上面的组件树中,A组件与B组件之间隔着很是多的组件,假如A组件但愿传递给B组件一个属性,那么不得不使用props将属性从A组件历经一系列中间组件最终跋山涉水传递给B组件。这样代码不只很是的麻烦,更重要的是中间的组件可能压根就用不上这个属性,却要承担一个传递的职责,这是咱们不但愿看见的。Context出现的目的就是为了解决这种场景,使得咱们能够直接将属性从A组件传递给B组件。react

Legacy Context

  这里所说的老版本Context指的是React16.3以前的版本所提供的Context属性,在我看来,这种Context是以一种协商声明的方式使用的。做为属性提供者(Provider)须要显式声明哪些属性能够被跨层级访问而且须要声明这些属性的类型。而做为属性的使用者(Consumer)也须要显式声明要这些属性的类型。官方文档中给出了下面的例子:git

import React, {Component} from 'react';
import PropTypes from 'prop-types';

class Button extends React.Component {

    static contextTypes = {
        color: PropTypes.string
    };

    render() {
        return (
            <button style={{background: this.context.color}}> {this.props.children} </button>
        );
    }
}

class Message extends React.Component {
    render() {
        return (
            <div> {this.props.text} <Button>Delete</Button> </div>
        );
    }
}

class MessageList extends React.Component {
    static childContextTypes = {
        color: PropTypes.string
    };

    getChildContext() {
        return {color: "red"};
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return <div>{children}</div>;
    }
}
复制代码

  咱们能够看到MessageList经过函数getChildContext显式声明提供color属性,而且经过静态属性childContextTypes声明了该属性的类型。而Button经过静态属性contextTypes声明了要使用属性的类型,两者经过协商的方式约定了跨层级传递属性的信息。Context确实很是方便的解决了跨层级传递属性的状况,可是为何官方却不推荐使用呢?github

  首先Context的使用是与React可复用组件的逻辑背道而驰的,在React的思惟中,全部组件应该具备复用的特性,可是正是由于Context的引入,组件复用的使用变得严格起来。就以上面的代码为例,若是想要复用Button组件,必须在上层组件中含有一个能够提供String类型的colorContext,因此复用要求变得严格起来。而且更重要的是,当你尝试修改Context的值时,可能会触发不肯定的状态。咱们举一个例子,咱们将上面的MessageList稍做改造,使得Context内容能够动态改变:redux

class MessageList extends React.Component {

    state = {
        color: "red"
    };

    static childContextTypes = {
        color: PropTypes.string
    };

    getChildContext() {
        return {color: this.state.color};
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return (
            <div> <div>{children}</div> <button onClick={this._changeColor}>Change Color</button> </div>
        );
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.state.color) + 1) % 3;
        this.setState({
            color: colors[index]
        });
    }
}
复制代码

  上面的例子中咱们MessageList组件Context提供的color属性改为了state的属性,当每次使用setState刷新color的时候,子组件也会被刷新,所以对应按钮的颜色也会发生改变,一切看起来是很是的完美。可是一旦组件间的组件存在生命周期函数ShouldComponentUpdate那么一切就变得诡异起来。咱们知道PureComponent实质就是利用ShouldComponentUpdate避免没必要要的刷新的,所以咱们能够对以前的例子作一个小小的改造:安全

class Message extends React.PureComponent {
    render() {
        return (
            <div> {this.props.text} <Button>Delete</Button> </div>
        );
    }
}
复制代码

  你会发现即便你在MessageList中改变了Context的值,也没法致使子组件中按钮的颜色刷新。这是由于Message组件继承自PureComponent,在没有接受到新的props改变或者state变化时生命周期函数shouldComponentUpdate返回的是false,所以Message及其子组件并无刷新,致使Button组件没有刷新到最新的颜色。ide

  若是你的Context值是不会改变的,或者只是在组件初始化的时候才会使用一次,那么一切问题都不会存在。可是若是须要改变Context的状况下,如何安全使用呢? Michel Weststrate在How to safely use React context 一文中介绍了依赖注入(DI)的方案。做者认为咱们不该该直接在getChildContext中直接返回state属性,而是应该像依赖注入(DI)同样使用conext。函数

class Theme {
    constructor(color) {
        this.color = color
        this.subscriptions = []
    }

    setColor(color) {
        this.color = color
        this.subscriptions.forEach(f => f())
    }

    subscribe(f) {
        this.subscriptions.push(f)
    }
}

class Button extends React.Component {
    static contextTypes = {
        theme: PropTypes.Object
    };

    componentDidMount() {
        this.context.theme.subscribe(() => this.forceUpdate());
    }

    render() {
        return (
            <button style={{background: this.context.theme.color}}> {this.props.children} </button>
        );
    }
}

class MessageList extends React.Component {

    constructor(props){
        super(props);
        this.theme = new Theme("red");
    }

    static childContextTypes = {
        theme: PropTypes.Object
    };

    getChildContext() {
        return {
            theme: this.theme
        };
    }

    render() {
        const children = this.props.messages.map((message) =>
            <Message text={message.text} />
        );
        return (
            <div> <div>{children}</div> <button onClick={this._changeColor}>Change Color</button> </div>
        );
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.theme.color) + 1) % 3;
        this.theme.setColor(colors[index]);
    }
}
复制代码

  在上面的例子中咱们创造了一个Theme类用来管理样式,而后经过ContextTheme的实例向下传递,在Button中获取到该实例而且订阅样式变化,在样式变化时调用forceUpdate强制刷新达到刷新界面的目的。固然上面的例子只是一个雏形,具体使用时还须要考虑到其余的方面内容,例如在组件销毁时须要取消监听等方面。学习

  回顾一下以前版本的Context,配置起来仍是比较麻烦的,尤为还须要在对应的两个组件中分别使用childContextTypescontextTypes的声明Context属性的类型。并且其实这两个类型声明并不能很好的约束context。举一个例子,假设分别有三个组件: GrandFather、Father、Son,渲染顺序分别是:

GrandFather -> Father -> Son

  那么假设说组件GrandFather提供的context是类型为number键为value的值1,而Father提供也是类型为number的键为value的值2,组件Son声明得到的是类型为number的键为value的context,咱们确定知道组件Son中this.context.value值为2,由于context在遇到同名Key值时确定取的是最靠近的父组件。

  一样地咱们假设件GrandFather提供的context是类型为string键为value的值"1",而Father提供是类型为number的键为value的值2,组件Son声明得到的是类型为string的键为value的context,那么组件Son会取到GrandFather的context值吗?事实上并不会,仍然取到的值是2,只不过在开发过程环境下会输出:

Invalid context value of type number supplied to Son, expected string

  所以咱们能得出静态属性childContextTypescontextTypes只能提供开发的辅助性做用,对实际的context取值并不能起到约束性的做用,即便这样咱们也不得不重复体力劳动,一遍遍的声明childContextTypescontextTypes属性。

New Context

  新的Context发布于React 16.3版本,相比于以前组件内部协商声明的方式,新版本下的Context大不相同,采用了声明式的写法,经过render props的方式获取Context,不会受到生命周期shouldComponentUpdate的影响。上面的例子用新的Context改写为:

import React, {Component} from 'react';

const ThemeContext = React.createContext({ theme: 'red'});

class Button extends React.Component {
    render(){
        return(
            <ThemeContext.Consumer>
                {({color}) => {
                    return (
                        <button style={{background: color}}>
                            {this.props.children}
                        </button>
                    );
                }}
            </ThemeContext.Consumer>
        );
    }
}

class Message extends React.PureComponent {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        );
    }
}

class MessageList extends React.Component {

    state = {
        theme: { color: "red" }
    };

    render() {
        return (
            <ThemeContext.Provider value={this.state.theme}>
                <div>
                    {this.props.messages.map((message) => <Message text={message.text}/>)}
                    <button onClick={this._changeColor}>Change Color</button>
                </div>
            </ThemeContext.Provider>
        )
    }

    _changeColor = () => {
        const colors = ["red", "green", "blue"];
        const index = (colors.indexOf(this.state.theme.color) + 1) % 3;
        this.setState({
            theme: {
                color: colors[index]
            }
        });
    }
}
复制代码

  咱们能够看到新的Context使用React.createContext的方式建立了一个Context实例,而后经过Provider的方式提供Context值,而经过Consumer配合render props的方式获取到Context值,即便中间组件中存在shouldComponentUpdate返回false,也不会致使Context没法刷新的问题,解决了以前存在的问题。咱们看到在调用React.createContext建立Context实例的时候,咱们传入了一个默认的Context值,该值仅会在Consumer在组件树中没法找到匹配的Provider才会使用,所以即便你给Providervalue传入undefined值时,Consumer也不会使用默认值。

  新版的Context API相比于以前的Context API更符合React的思想,而且能解决componentShouldUpdate的带来的问题。与此同时你的项目须要增长专门的文件来建立Context。在 React v17 中,可能就会删除对老版 Context API 的支持,因此仍是须要尽快升级。最后讲了这么多,可是在项目中仍是要尽可能避免Context的滥用,不然会形成组件间依赖过于复杂。

相关文章
相关标签/搜索