迦南

React应用中的Context详解

迦南 · 2017-05-15翻译 · 975阅读 原文链接

许多使用React的开发者们对于什么是context以及它存在的意义感到疑惑. 它也是React的一个特性,曾经在React文档中隐藏掉了, 虽然现在已经放到了React的官方文档中,但是我想写一篇解释context用处的文章会比较有用。

简单来说,你应该尽可能少的在你的组件中使用context。 但是如果你要用React写一个库,context是很有用的,这一点我们稍后再做讨论。

React中的context是什么,它是怎样工作的?

在React中组件之间交流依靠“properties”,简称“props”。父组件可传递props给他的子组件:

const ParentComponent = () => {
  const foo = 2
  return (
    <ChildComponent foo={foo} />
  )
}

上例中,父组件ParentCOmponent给子组件ChildComponent传递了propsfoo

子组件是指由其他组件渲染的组件,父组件是直接渲染的组件。

如果子组件想要和父组件通信,也可以通过props,最常见的是父组件传递给子组件一个回调,当需要通信时,子组件调用回调函数:

const ParentComponent = () => {
  const letMeKnowAboutSomeThing = () => console.log('something happened!')

  return (
    <ChildComponent letMeKnowAboutSomeThing={letMeKnowAboutSomeThing} />
  )
}

const ChildComponent = props => {
  const onClick = e => {
    e.preventDefault()
    props.letMeKnowAboutSomeThing()
  }

  return <a onClick={onClick}>Click me!</a>
}

通信的关键在于它是明确的。通过上面的代码,你可以知道组件之间如何通信,letMeKnowAboutSomeThing函数来自于哪里,谁调用了它,以及哪两个组件在通信。 可以在CodePen上看到完整代码.

数据的单向传递特性,是React最好的特性之一。React有着非常明确的规则,这使得代码非常易于维护和调试。当出现bug的时候,你只需要沿着props的传递路径去寻找问题就可以了。

这张图显示了当应用有许多层的时候props如何保持组件间清晰的通信;每个组件都不得不向子组件传递props。

在大型的应用中,你也许会遇见这样一个问题:需要将props从父组件ParentComponent传递到嵌套的很深的子组件ChildComponent中去。中间的组件可能并不会用到这些props,甚至可能并不知道这些props。当出现这种情况的时候,可以考虑使用React中的context。

在你的应用中,Context就像一个接口,让你不用传递props,子组件就能获取到父组件的数据。

当一个组件在它的context中定义了数据之后,这个组件的所有子节点都可以使用这个数据。这就意味着不用传递props,组件的所有后代都可以从context中获取到数据。让我们看看context是如何工作的。

如何在React应用中使用Context

首先,在父组件中,定义两个属性:

  1. 函数getChildContext,它定义了开放给子节点的context。

  2. 属性childContextTypes,它定义了getChildContext返回的对象类型。

对于一个为子代提供context的组件来说,上面的两点必须定义。下面是一个父组件在context中定义foo属性的例子:

class ParentComponent extends React.Component {
  getChildContext() {
    return { foo: 'bar' }
  }

  render() {
    return <ChildComponent />
  }
}

ParentComponent.childContextTypes = {
  foo: React.PropTypes.string
}

ChildComponent 现在可以通过定义属性 contextTypes来获取context中的foo了:

const ChildComponent = (props, context) => {
  return <p>The value of foo is: { context.foo }</p>
}
ChildComponent.contextTypes = {
  foo: React.PropTypes.string
}

在一个函数式,无状态的组件中,context通过第二个函数参数传入。在标准的用class定义的组件中,可以使用this.context定义context。

重要的一点是,所有的子组件,或者子组件的子组件都可以通过定义contextTypes获取到相同的context。

为什么你应该拒绝使用context

这有几点你应该拒绝使用context的原因。

1. 很难找到数据源

想象一下你正在为一个有几百个组件的大型应用写组件。某一个组件出现了bug,所以在调试的时候,你会发现有一些组件使用了context,并且它们输出的数据是错误的。

const SomeAppComponent = (props, context) => (
  <div>
    <p>Hey user, the current value of something is { context.value }</p>
    <a onClick={context.onSomeClick()}>Click here to change it.</a>
  </div>
)

SomeAppComponent.contextTypes = {
  value: React.PropTypes.number.isRequired,
  onSomeClick: React.PropTypes.func.isRequired,
}

这是一个关于点击事件没有更新正确的数据的bug,所以你需要去查看一下onSomeClick函数。如果这个函数是作为props从父组件传递过来的,你可以很快找到这个函数在哪里定义(通常只需要通过名字查找就好),并开始调试。但是如果用了context,你只能祈祷能通过函数名称来找到组件。这种方法最终肯定是能找到的,但是当你的应用很大的时候,很难快速的定位。

这个问题就类似于使用面向对象的编程语言中继承类。类继承的越多(或者在React中,组件树越深),就越难定位一个函数是从哪里继承的。

2. 将组件与特定的父组件绑定

一个接收props(或者不接收props)的组件可以任意复用,只要在使用时传入组件需要的props即可。

但是如果一个组件需要特定的context,那么在使用它的时候,就需要定义能提供context的父组件。这种组件很难复用,应为你在使用它的时候,必须保证有一个父组件能提供所需context。

3. 难以测试

与前一点相关,使用context的组件增加了测试的难度。以下是针对一个接受propsfoo的组件写的测试,使用了Enzyme

`const wrapper = mount(<SomeComponent foo='bar' />)`

接下来是针对使用context的组件写的测试:

class ParentWithContext extends React.Component {
  getChildContext() {...}

  render() {
    return <SomeComponent />
  }
}
ParentWithContext.childContextTypes = {...}

const wrapper = mount(<ParentWithContext />)

这样的测试会更难因为我们不得不去创建一个正确的父组件 - 为了测试而给组件组件设置context环境,这会使测试非常冗余并且混乱。

其实你可以用Enzyme中的setContext给测试的组件设置context,但是我更倾向于不要使用这种会破环React结构的测试。同样的,对于其它测试框架,这种组件也不容易进行测试。

4. context的值的变化和渲染不清晰

对于React来说,使用state及props的组件将会在以下两种情况下渲染:

  1. 组件的props改变。

  2. this.setState函数被调用。

当以上两种情况发生时,getChildContext函数也会被调用,所以理论上讲,你可以依赖于context的改变来更新组件。但是问题出在shouldComponentUpdate上。任何组件都可以在不需要更新的情况下给shouldComponentUpdate的返回值定义为false。如果一个中间层的组件定义shouldComponentUpdate返回false,子组件不会更新,甚至context值也不会更新:

TopLevelComponent
- defines context.foo

    MidLevelComponent
    - defines `shouldComponentUpdate` to return `false`

        ChildComponent
        - renders `context.foo` into the DOM

在上面的例子中,因为父组件的shouldComponentUpdate返回为false,所以即使ChildComponentcontext.foo改变,组件也不会渲染。

什么时候应该用context

如果你是开源库的作者,那么context是很有用的。比如说 React Router中的组件就使用了context。当你在写一个库的时候,组件之间如果需用通信或互传数据,context是完美的选择。另外一个使用了context的著名库是react-redux。我建议你去看一下React Router和React Redux的源码,你能从中学到很多。

下面让我们来写一个路由库,RubbishRouter。这个库中将会定义两个组件:RouterRouteRouter需要在context中定义一个router对象,这样我们的Route组件可以获取到数据并且在函数中使用。

Router将用来包裹整个应用,然后我们用一些Route组件定义当URL匹配时,应该被渲染的组件,每个Route都有一个path属性,表明渲染前应该匹配的路由。

首先,Router。它在conetxt中定义了router对象,不同于简单的渲染子组件:

const { Component, PropTypes } = React

class Router extends Component {
  getChildContext() {
    const router = { register(url) { console.log('registered route!', url) } }
    return { router: router }
  }
  render() { return <div>{this.props.children}</div> }
}
Router.childContextTypes = {
  router: PropTypes.object.isRequired,
}

Route需要找到this.context.router,并且在渲染的时候注册自己的路径:

class Route extends Component {
  componentWillMount() {
    this.context.router.register(this.props.path)
  }
  render() {
    return <p>I am the route for {this.props.path}</p>
  }
}
Route.contextTypes = {
  router: PropTypes.object.isRequired,
}

最后,我们可以在app中使用RouterRoute组件:

const App = () => (
  <div>
    <Router>
      <div>
        <Route path="/foo" />
        <Route path="/bar" />
        <div>
          <Route path="/baz" />
        </div>
      </div>
    </Router>
  </div>
)

在这种情况下context的好处在于,作为一个作者,你可以在任意情况下给组件提供数据,而不用去关注它在哪里渲染。只要Route嵌套在Router里面,我们并不用约束开发者使用特定的结构。

结论

希望这篇博客能教会你如何使用context以及为什么大多数情况下要避开使用它。

感谢以下的博客和文档给了我非常重要的参考,同时贴出链接:

你也可以来Arnaud Rinquin重温这篇博客。

译者迦南尚未开通打赏功能

相关文章