上下文 目前是 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
的新方法,它定义了一个新的上下文并返回 Provider
和 Consumer
。
Provider
建立了所有子组件都可以挂钩的上下文。 它通过 Consumer
挂钩,Consumer
使用渲染属性。 该渲染属性函数的第一个参数是 value
,该值已赋予 Provider
。 通过更新 Provider
中的值,所有消费者都会更新以反映新值。
使用新的上下文带来的一个额外好处是,我们不再需要使用 childContextTypes
、getChildContext
和 contextTypes
。
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
的必要,并为以可重用方式定义新上下文打开了可能性。
所以这 1) 解决了一个已经被 flux/redux 解决的问题 2) 增加了完成同样事情所需的代码行数 3) 增加了作为开发人员我需要学习的内容数量 4) 仍然存在一些问题。为什么我们作为开发人员要这样做?
1) 这没有道理。大多数 Flux/Redux 实现都在内部使用 React Context。React Context 正在获得一个新的 API,该 API 消除了旧 API 周围的注意事项,因此理论上 Flux/Redux 实现也可以从中受益。最重要的是,如果你使用 Flux/Redux 只是为了避免传递 props,那么你使用它们的方式是错误的。你应该直接使用 Context。
2) 在许多情况下,它减少了所需的代码行数。但无论如何,新 API 的清晰度/明确性应该值得增加的行数。
3) 哦,我的天。只忘记旧的 API 并重新利用释放的脑容量用于新的 API?;)
4) 你真的读完这篇文章了吗?
@saynothingetal 我相信这很明显,但“上下文”的意义在于尽可能地保持 react 的 VIEW 为原生。虽然我们都不得不经历 flux 和 Redux 模式的全过程,但它们通常是“非意见化”(它们不是为 react “构建”的)。在尝试安装 ReactJS 库并开始你的视图时,这可能很麻烦。因此,虽然你可能会因学习新的“上下文”API 而感到沮丧,但我建议坚持使用它,因为你很少需要实现 Redux/Flux 库,并且这个 API “应该”使区分是否使用它们变得不那么模糊。
很棒的文章,我只是想建议可能要创建关于如何导出你的消费者的示例(我怀疑每个人在使用 React 时都会编写一个文件,我知道我花了些时间进行试错才能让 React.Consumer 传递我的状态)。
是的,当然!这是一个很好的建议。新的上下文 API 的优点在于它高度可重用,因此我们可以将 Provider 和 Consumer 存储在一个文件中,并在任何需要的地方使用它们。
对不起,我不明白为什么新的上下文 API 会是一个好消息?
它不会破坏数据流的理解吗?
我如何知道数据来自哪里?只有通过 ctrl+f 搜索相同的上下文提供者名称吗?
它让我想起了数据中的 GOTO。
如果通过多个组件传递数据变得很困难,为什么不直接传递组件呢?
https://medium.com/@RubenOostinga/avoiding-deeply-nested-component-trees-973edb632991
谢谢
Step,这些都是有趣的问题。让我们看看是否可以将它们分解一些。
1) 为什么新的上下文 API 是一个好消息?
对我来说,这是因为新的 API 与旧的 API 相比提供了更多功能。不再需要在父级上定义 getChildContext 方法。不再需要在每个地方定义一堆与上下文相关的道具类型。它在精神上不那么累,也更容易弄清楚发生了什么。我还可以重用相同的上下文提供者/消费者,并且它会正常工作。
2) 它会破坏数据流的理解吗?
我认为是的,它确实在一定程度上破坏了它。特别是,如果你有深度嵌套的组件,必须向上回溯并跟踪上下文的定义位置可能很麻烦。尽管有人可能会争辩说,这与道具钻取一样在精神上很累。
希望,在这种情况下,你将在组件树的更高位置有一个上下文提供者,或者至少在一个文件中使用多个提供者,以便更容易更新。
3) 我如何知道数据来自哪里?
我想说这取决于你想要完成什么。如果你正在构建一个复杂的应用程序,你可能应该使用 redux(它也在后台使用上下文)。
至少对我来说,上下文可以用于更一次性的用例。例如,更改应用程序主题是使用上下文 API 的一个常见用例。
4) 为什么不传递组件?
这是一个很好的观点,并且通常可以做到。我认为这种方法的一个问题是深度嵌套在另一个组件中的组件。我们通常没有权访问树中深处以执行此操作的组件,并且必须在每个假设的 UI 状态下正确嵌套组件,这将比用上下文包装它并让它根据该上下文反映要困难得多。