[译]React高级话题之Context

前言

本文为意译,翻译过程当中掺杂本人的理解,若有误导,请放弃继续阅读。javascript

原文地址:Contexthtml

Context提供了一种不须要手动地经过props来层层传递的方式来传递数据。java

正文

在典型的React应用中,数据是经过props,自上而下地传递给子组件的。可是对于被大量组件使用的固定类型的数据(好比说,本地的语言环境,UI主题等)来讲,这么作就显得十分的累赘和笨拙。Context提供了一种在组件之间(上下层级关系的组件)共享这种类型数据的方式。这种方式不须要你手动地,显式地经过props将数据层层传递下去。node

何时用Context?

这一小节,讲的是context适用的业务场景。react

Context是为那些能够认定为【整颗组件树范围内能够共用的数据】而设计的。好比说,当前已认证的用户数据,UI主题数据,当前用户的偏好语言设置数据等。举个例子,下面的代码中,为了装饰Button component咱们手动地将一个叫“theme”的prop层层传递下去。 传递路径是:App -> Toolbar -> ThemedButton -> Button算法

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}
复制代码

使用context,咱们能够跳过层层传递所通过的中间组件。如今咱们的传递路径是这样的:App -> Button缓存

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
复制代码

在你用Context以前

这一小节,讲的是咱们要慎用context。在用context以前,咱们得考虑一下当前的业务场景有没有第二种技术方案可用。只有在确实想不出来了,才去使用context。babel

Context主要用于这种业务场景:大量处在组件树不一样层级的组件须要共享某些数据。实际开发中,咱们对context要常怀敬畏之心,谨慎使用。由于它犹如潘多拉的盒子,一旦打开了,就形成不少难以控制的现象(在这里特指,context一旦滥用了,就会形成不少组件难以复用)。app

若是你只是单纯想免去数据层层传递时对中间层组件的影响,那么组件组合是一个相比context更加简单的技术方案。ide

举个例子来讲,假如咱们有一个叫Page的组件,它须要将useravatarSize这两个prop传递到下面好几层的Link组件和Avatar组件:

<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} /> </Link>
复制代码

咱们大费周章地将useravatarSize这两个prop传递下去,最终只有Avatar组件才真正地用到它。这种作法显得有点低效和多余的。假如,到后面Avatar组件须要从顶层组件再获取一些格外的数据的话,你还得手动地,逐层地将这些数据用prop的形式来传递下去。实话说,这真的很烦人。

不考虑使用context的前提下,另一种能够解决这种问题的技术方案是:Avatar组件做为prop传递下去。这样一来,其余中间层的组件就不要知道user这个prop的存在了。

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// Now, we have:
<Page user={user} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
复制代码

经过这个改动,只有最顶层的组件Page须要知道Link组件和Avatar组件须要用到“user”和“avatarSize”这两个数据集。

在不少场景下,这种经过减小须要传递prop的个数的“控制反转”模式让你的代码更干净,并赋予了最顶层组件更多的控制权限。然而,它并不适用于每个业务场景。由于这种方案会增长高层级组件的复杂性,并以此为代价来使得低层家的组件来变得更加灵活。而这种灵活性每每是过分的。

在“组件组合”这种技术方案中,也没有说限定你一个组件只能有一个子组件,你可让父组件拥有多个的子组件。或者甚至给每一个单独的子组件设置一个单独的“插槽(slots)”,正如这里所介绍的那样。

function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}
复制代码

这种模式对于大部分须要将子组件从它的父组件中分离开来的场景是足够有用的了。若是子组件在渲染以前须要与父组件通信的话,你能够进一步考虑使用render props技术。

然而,有时候你须要在不一样的组件,不一样的层级中去访问同一份数据,这种状况下,仍是用context比较好。Context负责集中分发你的数据,在数据改变的同时,能将新数据同步给它下面层级的组件。第一小节给出的范例中,使用context比使用本小节所说的“组件组合”方案更加的简单。适用context的场景还包括“本地偏好设置数据”共享,“UI主题数据”共享和“缓存数据”共享等。

相关API

React.createContext

const MyContext = React.createContext(defaultValue);
复制代码

该API是用于建立一个context object(在这里是指Mycontext)。当React渲染一个订阅了这个context object的组件的时候,将会从离这个组件最近的那个Provider组件读取当前的context值。

建立context object时传入的默认值只有组件在上层级组件树中没有找到对应的的Provider组件的时候时才会使用。这对于脱离Provider组件去单独测试组件功能是颇有帮助的。注意:若是你给Provider组件value属性提供一个undefined值,这并不会引用React使用defaultValue做为当前的value值。也就是说,undefined仍然是一个有效的context value。

Context.Provider

<MyContext.Provider value={/* some value */}>
复制代码

每个context object都有其对应的Provider组件。这个Provider组件使得Consumer组件可以订阅并追踪context数据。

它接受一个叫value的属性。这个value属性的值将会传递给Provider组件全部的子孙层级的Consumer组件。这些Consumer组件会在Provider组件的value值发生变化的时候获得从新渲染。从Provider组件到其子孙Consumer组件的这种数据传播不会受到shouldComponentUpdate(这个shouldComponentUpdate应该是指Cousumer组件的shouldComponentUpdate)这个生命周期方法的影响。因此,只要父Provider组件发生了更新,那么做为子孙组件的Consumer组件也会随着更新。

断定Provider组件的value值是否已经发生了变化是经过使用相似于Object.is算法来对比新旧值实现的。

注意:当你给在Provider组件的value属性传递一个object的时候,用于断定value是否已经发生改变的法则会致使一些问题,见注意点

Class.contextType

译者注:官方文档给出的关于这个API的例子我并无跑通。不知道是我理解错误仍是官方的文档有误,读者谁知道this.context在new context API中是如何使用的,麻烦在评论区指教一下。

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* perform a side-effect at mount using the value of MyContext */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* render something based on the value of MyContext */
  }
}
MyClass.contextType = MyContext;
复制代码

组件(类)的contextType静态属性能够赋值为一个context object。这使得这个组件类能够经过this.context来消费离它最近的context value。this.context在组件的各类生命周期方法都是可访问的。

注意:

  1. 使用这个API,你只能够订阅一个context object。若是你须要读取多个context object,那么你能够查看Consuming Multiple Contexts
  2. 若是你想使用ES7的实验性特征public class fields syntax,你可使用static关键字来初始化你的contextType属性:
class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* render something based on the value */
  }
}
复制代码

Context.Consumer

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>
复制代码

Consumer组件是负责订阅context,并跟踪它的变化的组件。有了它,你就能够在一个function component里面对context发起订阅。

如上代码所示,Consumer组件的子组件要求是一个function(注意,这里不是function component)。这个function会接收一个context value,返回一个React node。这个context value等同于离这个Consumer组件最近的Provider组件的value属性值。假如Consumer组件在上面层级没有这个context所对应的Provider组件,则function接收到的context value就是建立context object时所用的defaultValue。

注意:这里所说的“function as a child”就是咱们所说的render props模式。

示例

1. 动态context

我在这个例子里面涉及到this.context的组件的某个生命周期方法里面打印console.log(this.context),控制台打印出来是空对象。从界面来看,DOM元素button也没有background。

这是一个关于动态设置UI主题类型的context的更加复杂的例子:

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // default value
);
复制代码

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button {...props} style={{backgroundColor: theme.background}} /> ); } } ThemedButton.contextType = ThemeContext; export default ThemedButton; 复制代码

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// An intermediate component that uses the ThemedButton
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}> Change Theme </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    // The ThemedButton button inside the ThemeProvider
    // uses the theme from state while the one outside uses
    // the default dark theme
    // 以上注释所说的结果,我并无看到。
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);
复制代码

2. 在内嵌的组件中更新context

组件树的底层组件在不少时候是须要更新Provider组件的context value的。面对这种业务场景,你能够在建立context object的时候传入一个function类型的key-value,而后伴随着context把它传递到Consumer组件当中:

theme-context.js

// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
复制代码

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // The Theme Toggler Button receives not only the theme
  // but also a toggleTheme function from the context
  return (
    <ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> ); } export default ThemeTogglerButton; 复制代码

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State also contains the updater function so it will
    // be passed down into the context provider
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // The entire state is passed to the provider
    return (
      <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root); 复制代码

3. 同时消费多个context

为了使得context所致使的从新渲染的速度更快,React要求咱们对context的消费要在单独的Consumer组件中去进行。

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext({
  name: 'Guest',
});

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

    // App component that provides initial context values
    // 两个context的Provider组件嵌套
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// A component may consume multiple contexts
function Content() {
  return (
     // 两个context的Consumer组件嵌套
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}
复制代码

可是假如两个或以上的context常常被一同消费,这个时候你得考虑合并它们,使之成为一个context,并建立一个接受多个context做为参数的render props component。

注意点

由于context是使用引用相等(reference identity)来判断是否须要re-redner的,因此当你给Provider组件的value属性提供一个字面量javascript对象值时,这就会致使一些性能问题-consumer组件发生没必要要的渲染。举个例子,下面的示例代码中,全部的consumer组件将会在Provider组件从新渲染的时候跟着一块儿re-render。这是由于每一次value的值都是一个新对象。

class App extends React.Component {
  render() {
    return (
     // {something: 'something'} === {something: 'something'}的值是false
      <Provider value={{something: 'something'}}>
        <Toolbar /> </Provider>
    );
  }
}
复制代码

为了不这个问题,咱们能够把这种引用类型的值提高到父组件的state中去:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},
    };
  }

  render() {
    return (
      <Provider value={this.state.value}> <Toolbar /> </Provider>
    );
  }
}
复制代码

遗留的API

React在先前的版本中引入了一个实验性质的context API。相比当前介绍的这个context API,咱们称它为老的context API。这个老的API将会被支持到React 16.x版本结束前。可是你的app最好将它升级为上文中所介绍的新context API。这个遗留的API将会在将来的某个大版本中去除掉。想了解更多关于老context API,查看这里

相关文章
相关标签/搜索