用 React 将事物置于上下文中

Avatar of Neal Fennimore
Neal Fennimore

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元免费信用额度!

上下文 目前是 React 的一个实验性 API,但很快就会成为 一等公民! 它有很多有趣的原因,但也许最重要的是它允许父组件将数据隐式地传递给子组件,无论组件树有多深。 换句话说,数据可以添加到父组件中,然后任何子组件都可以访问它。

查看 CodePen 上 Neal Fennimore (@nealfennimore) 的 React 上下文灯光

虽然这通常是使用类似 Redux 的东西时的用例,但如果您不需要复杂的数据管理,它非常方便。 想想看! 我们创建了一个自定义数据下游,决定哪些属性被传递以及在哪个级别传递。 太酷了。

上下文非常适合在您有很多依赖于单个数据片段的组件但位于组件树深处的区域。 显式地将每个属性传递给每个单独的组件通常会让人不知所措,而使用上下文在这里会容易得多。

例如,让我们考虑如何通常将属性向下传递到树中。 在这种情况下,我们使用每个组件上的属性传递颜色 red 以将其向下游移动。

class Parent extends React.Component {
  render(){
    return <Child color="red" />;
  }
}

class Child extends React.Component {
  render(){
    return <GrandChild color={this.props.color} />
  }
}

class GrandChild extends React.Component {
  render(){
    return (
      <div style={{color: this.props.color}}>
        Yep, I'm the GrandChild
      </div>
    );
  }
}

如果我们根本不想让 Child 组件拥有该属性怎么办? 上下文使我们不必通过 Child 组件传递颜色,而是可以直接从 Parent 传递到 GrandChild

class Parent extends React.Component {
  // Allow children to use context
  getChildContext() {
    return {
      color: 'red'
    };
  }
  
  render(){
    return <Child />;
  }
}

Parent.childContextTypes = {
  color: PropTypes.string
};

class Child extends React.Component {
  render() {
    // Props is removed and context flows through to GrandChild
    return <GrandChild />
  }
}

class GrandChild extends React.Component {
  render() {
    return (
      <div style={{color: this.context.color}}>
        Yep, I'm still the GrandChild
      </div>
    );
  }
}

// Expose color to the GrandChild
GrandChild.contextTypes = {
  color: PropTypes.string
};

虽然稍微冗长一些,但好处是可以将 color 公开到组件树中的任何位置。 好吧,有时会这样…

有一些需要注意的地方

您不能总是既要鱼,又要熊掌,当前形式的上下文也不例外。 如果您最终将上下文用于除最简单情况以外的所有情况,那么您很有可能遇到一些潜在的问题。

上下文非常适合用于初始渲染。 在运行时更新上下文? 不太可能。 上下文的一个常见问题是上下文更改并不总是反映在组件中。

让我们更详细地分析这些需要注意的地方。

需要注意的地方 1:使用纯组件

使用 PureComponent 时,上下文很难,因为默认情况下它不会对上下文执行任何浅层比较。 浅层比较PureComponent 正在测试对象的值是否严格相等。 如果它们不相等,那么(仅当且仅当)组件才会更新。 但是由于没有检查上下文,所以……什么也不会发生。

查看 CodePen 上 Neal Fennimore (@nealfennimore) 的 带有 PureComponents 的 React 上下文灯光

需要注意的地方 2:组件应该更新吗? 也许吧。

如果组件的 shouldComponentUpdate 返回 false,上下文也不会更新。 如果您有自定义 shouldComponentUpdate 方法,那么您还需要考虑上下文。 为了使用上下文启用更新,我们可以使用自定义 shouldComponentUpdate 更新每个单独的组件,该方法类似于以下内容。

import shallowEqual from 'fbjs/lib/shallowEqual';

class ComponentThatNeedsColorContext extends React.PureComponent {
  // nextContext will show color as soon as we apply ComponentThatNeedsColorContext.contextTypes
  // NOTE: Doing the below will show a console error come react v16.1.1
  shouldComponentUpdate(nextProps, nextState, nextContext){
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState) || !shallowEqual(this.context, nextContext);
  }
}

ComponentThatNeedsColorContext.contextTypes = {
  color: PropTypes.string
};

但是,这并不能解决父级和子级之间中间 PureComponent 阻止上下文更新的问题。 这意味着父级和子级之间的每个 PureComponent 都需要定义 contextTypes,并且它们还需要具有更新的 shouldComponentUpdate 方法。 而且,就这一点而言,这对获得微不足道的收益而言付出了太多努力。

处理需要注意的地方的更好方法

幸运的是,我们有一些方法可以解决这些需要注意的地方。

方法 1:使用高阶组件

一个 高阶组件 可以从上下文中读取并作为属性将所需的值传递给下一个组件。

import React from 'react';

const withColor = (WrappedComponent) => {
    class ColorHOC extends React.Component {
        render() {
            const { color } = this.context;        
            return <WrappedComponent style={{color: color}} {...this.props} />
        }
    }
         
    ColorHOC.contextTypes = {
        color: React.PropTypes.string  
    };

    return ColorHOC;
};


export const Button = (props)=> <button {...props}>Button</button>

// ColoredButton will render with whatever color is currently in context with a style prop
export const ColoredButton = withColor( Button );

查看 CodePen 上 Neal Fennimore (@nealfennimore) 的 带有 HOC 的 React 上下文灯光

方法 2:使用渲染属性

渲染属性 允许我们使用属性在两个组件之间共享代码。

class App extends React.Component {
    getChildContext() {
        return {
            color: 'red'
        }
    }

    render() {
        return <Button />
    }
}

App.childContextTypes = {
    color: React.PropTypes.string
}

// Hook 'Color' into 'App' context
class Color extends React.Component {
    render() {
        return this.props.render(this.context.color);
    }
}

Color.contextTypes = {
    color: React.PropTypes.string
}

class Button extends React.Component {
    render() {
        return (
            <button type="button">
                {/* Return colored text within Button */}
                <Color render={ color => (
                    <Text color={color} text="Button Text" />
                ) } />
            </button>
        )
    }
}

class Text extends React.Component {
    render(){
        return (
            <span style={{color: this.props.color}}>
                {this.props.text}
            </span>
        )
    }
}

Text.propTypes = {
    text: React.PropTypes.string,
    color: React.PropTypes.string,
}

方法 3:依赖注入

我们解决这些需要注意的地方的第三种方法是使用 依赖注入 来限制上下文 API 并允许组件根据需要订阅。

新的上下文

新的上下文使用方式(目前计划用于 React(16.3) 的下一个次要版本)具有可读性更好且更容易编写而不会出现以前版本中的“需要注意的地方”的优点。 我们现在有了一个名为 createContext 的新方法,它定义了一个新的上下文并返回 ProviderConsumer

Provider 建立了所有子组件都可以挂钩的上下文。 它通过 Consumer 挂钩,Consumer 使用渲染属性。 该渲染属性函数的第一个参数是 value,该值已赋予 Provider。 通过更新 Provider 中的值,所有消费者都会更新以反映新值。

使用新的上下文带来的一个额外好处是,我们不再需要使用 childContextTypesgetChildContextcontextTypes

const ColorContext = React.createContext('color');
class ColorProvider extends React.Component {
    render(){
        return (
            <ColorContext.Provider value={'red'}>
                { this.props.children }
            </ColorContext.Provider>
        )
    }
}

class Parent extends React.Component {  
    render(){
        // Wrap 'Child' with our color provider
        return (
            <ColorProvider>
                <Child />
            </ColorProvider>
        );
    }
}

class Child extends React.Component {
    render(){
        return <GrandChild />
    }
}

class GrandChild extends React.Component {
    render(){
        // Consume our context and pass the color into the style attribute
        return (
            <ColorContext.Consumer>
                {/* 'color' is the value from our Provider */}
                {
                    color => (
                        <div style={{color: color}}>
                            Yep, I'm still the GrandChild
                        </div>
                    )
                }
            </ColorContext.Consumer>
        );
    }
}

单独的上下文

由于我们在如何公开上下文以及允许哪些组件使用上下文方面拥有更细粒度的控制,因此我们可以单独使用不同的上下文包装组件,即使它们位于同一个组件中。 我们可以在下一个示例中看到这一点,通过使用 LightProvider 两次,我们可以为两个组件提供单独的上下文。

查看 CodePen 上 Neal Fennimore (@nealfennimore) 的 带有新上下文的 React 上下文灯光

结论

上下文是一个强大的 API,但也很容易使用错误。 在使用它时也有一些注意事项,当组件出现问题时,可能很难找出问题所在。 虽然高阶组件和依赖注入为大多数情况提供了替代方案,但在代码库的孤立部分,上下文可以有效地使用。

然而,有了下一个上下文,我们不再需要担心之前版本中遇到的问题。它消除了在单个组件上定义contextTypes的必要,并为以可重用方式定义新上下文打开了可能性。