[译]React高级指引3:Context

原文连接:reactjs.org/docs/contex…html

引言

context提供了一种数据传输方式,它使得数据能够直接经过组件树传递而不须要在每个层级上手动地传递props。react

在典型的React应用中,数据是经过props自上而下(父组件传递给子组件)传递的,可是对于同时被许多组件所须要的某些props(如我的偏好,UI主题)来讲,使用这种方式传递数据简直就是受刑。Context提供了不须要显式地在组件树上每一个层级传递prop而是直接在组件之间传递的方法。算法

何时使用context

context设计的目的是为了共享那些对于组件树而言是“全局”的数据,好比当前用户信息,主题或语言等。在下面的示例代码中,咱们手动传递了一个“theme”prop来为Button组件提供样式。缓存

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

function Toolbar(props) {
  //Toolbar组件必需要传递一个额外
  //的prop“theme”给ThemedButton组件。
  //若是应用中的每一个按钮都须要知道theme是
  //什么的话,那么这会要人老命的,由于你须要在全部
  //组件中一个个传递。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

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

可是使用contex的话,咱们能够避免经过中间组件来传递props:bash

//context让咱们不须要在每个组件中显式地传递prop就
//能够之间将数据传递进位于组件树深层次的组件中。
//建立一个表示当前主题的context并赋予初始值light
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    //使用Provider将当前主题传递到当前组件树之下。
    任何组件均可以获取到这个值,不管它的层级有多深。
    //在本例中咱们把“dark“做为当前主题传递。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}
//位于中间层级的组件不须要再显式地传递主题了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  //将当前主题的contex值赋值给contextType
  //React会在当前层级之上找到最近的Provider并获取
  //它的值。在本例中,当前主题是”dark"。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } } 复制代码

使用context以前的考虑

context主要在位于不一样嵌套层级的组件须要获取同一个数据是使用。请谨慎地使用它由于context会使你的组件复用度变差。babel

若是你仅仅只想避免在过多的层级传递prop,那么组件组合是比context更简单的解决方案。数据结构

好比在下面的例子中,Page组件传递了useravatarSize给了几个层级之下的LinkAvatar组件:app

<Page user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ...  它渲染了 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>
复制代码

若是最后仅仅只有Avatar组件使用了useravatarSize,那么你可能会以为把它们传递那么多层级彻底不必。若是Avatar组件须要从顶层传递更多的prop,那么你可能会所以抓狂,由于你须要同时在全部的中间组件上都添加这些prop一遍。ide

不适用context解决这个问题的方法是把Avatar组件做为prop传递下去,这样中间组件就不须要知道其余关于user或avatarSize的信息了:函数

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

// 如今咱们将看到:
<Page user={user} avatarSize={avatarSize} />
// ... 它渲染了 ...
<PageLayout userLink={...} />
// ... 它渲染了 ...
<NavigationBar userLink={...} />
// ... 它渲染了 ...
{props.userLink}
复制代码

如今,只有最顶层的Page组件须要知道Link组件和Avatar组件须要使用useravatarSize

这对组件的控制反转经过减小传递的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}
    />
  );
}
复制代码

上述模式适用于大部分场景,在这些场景下你须要将子组件和直接父母组件解耦。若是子组件在渲染以前组要和父组件交互,你能够在这篇文章中获取相关知识。

而后有些时候某一数据被在组件树不一样嵌套层级的组件所须要。context可以让你“广播”这些数据,因此在这种状况下请直接使用context。使用context的场景一般是管理locale,theme和一些缓存数据,这比使用替代方案简单的多。

API

React.createContext

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

上述代码建立了一个context对象。当React渲染的组件使用了这个context对象时,React会从当前层级之上匹配最近的一个Provider来读取该context值。

参数defaultValue只在组件没有在上层组件树中找到匹配的Provider才会生效。这有助于在不包装组件的状况下测试它们。注意:传递undefined给Provider时消费组件的defaultValue不会生效。

Context.Provider

<MyContext.Provider value={/* 某个值 */}>
复制代码

每个context对象都返回一个React组件,它容许消费组件实时更新值的变化。

Provider接收一个value做为prop并将其传递给它的子消费组件。一个Provider能够和多个消费组件有对应关系。Provider之间也能够相互嵌套而且深层次的value值会覆盖其余的值。

当Provider的value值更新时,它内部的全部消费组件都会从新渲染。Provider和它内部组件的value值传递不受限于shouldComponentUpdate函数,所以当消费组件的祖先组件中止更新时它也能够更新。

根据新旧值来决定是否更新使用的是与Object.is相同的算法。

注意: 当传递value给对象时,检测数据变化的方法可能会致使一些问题,详情请查看注意事项。

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组
    件的值来执行一些有反作用的操做 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 根据MyContext的值渲染一些数据 */
  }
}
MyClass.contextType = MyContext;
复制代码

class的contextType属性会被重赋值为一个经过React.createContext()建立的context对象。这能让你经过使用this.context来消费最近的context上的值。你能够在任何生命周期方法中使用它,包括render方法。

注意: 经过这个API你只能订阅一个context。若是你须要订阅多个context,请查看使用多个context。 若是你在使用实验性的public class fileds语法,你可使用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 => /*根据context值渲染一些数据 */}
</MyContext.Consumer>
复制代码

这里,React组件也能够获取到context的变动,这能让你在函数式组件中完成订阅。

须要函数做为子元素。这个函数接收当前context的值并返回一个React的节点。value入参的值等同于高层级上最近的Provider的value值。若是在更高层级上没有对应的Provider,那么value入参的值等同于构建context时传入的defaultValue值。

注意: 须要了解关于函数做为子元素模式的更多内容,请查看render props

Context.displayName

context对象接收一个displayName字符串属性。React DevTools根据这个字符串来决定context要显式的值。

好比,下面的组件会在DevTools上显示MyDisplayName :

const MyContext = React.createContext(/* 一些值 */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools
复制代码

示例

动态context

对于上面的theme示例,使用动态值后更复杂的用法:

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';

// 一个使用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() {
    //在ThemeProvider内部的ThemedButton按钮
    //使用了state中存储的theme,而在Provider外部
    //的按钮使用了默认的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);
复制代码

在嵌套组件中更新context

在位于组件树深层的嵌套组件中更新context是很是重要的。在这种状况下你能够经过context传递一个函数来让消费组件更新context:

theme-context.js

//确保传递给createContext的数据结构与消费组件
//所须要的相匹配
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});
复制代码

theme-toggler-button.js

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

function ThemeTogglerButton() {
  //ThemeToggleButton组件不只从context接收了theme
  //还接收了toggleTheme函数
  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包含了更新函数
    //所以它也会被context provider传递下去
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 整个state都被provider传递下去
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

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

ReactDOM.render(<App />, document.root);

Consuming Multiple Contexts 
复制代码

使用多个context

为了保证context能快速地从新渲染,React须要每个consumer组件的context成为组件树上单独的节点。

// Theme context,默认值为light
const ThemeContext = React.createContext('light');

// 登陆用户context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;
    //App组件提供了context的初始值
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}

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

//一个组件可能会消费多个context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}
复制代码

若是两个以上的context值常常被一块儿使用,你就能够考虑构建可以同时提供这些值的渲染组件。

注意事项

由于context使用参考标识(reference identity)来决定何时渲染,这里可能会有一些陷阱,当provider的父组件从新渲染时,可能在consumer组件中触发一些无心识的渲染。好比下面的代码中每一次Provider从新渲染时因为value属性都会被赋予一个新的对象,在它之下的全部consumer组件都会从新渲染:

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

为了不这一点,把value值提高到父组件的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运行。老版的API在全部的16.x版本中都会获得支持,但用到它的应用应该迁移到新的版本。过期的API将会在将来的版本中被移除。阅读过期的context文档了解更多。

相关文章
相关标签/搜索