上下文(Context)

上下文(Context) 提供了一种通过组件树传递数据的方法,无需在每个级别手动传递 props 属性。

在典型的 React 应用程序中,数据通过 props 自上而下(父到子)传递,但对于应用程序中许多组件所需的某些类型的 props(例如环境偏好,UI主题),这可能很麻烦。 上下文(Context) 提供了在组件之间共享这些值的方法,而不必在树的每个层级显式传递一个 prop 。

何时使用 Context

Context 旨在共享一个组件树内可被视为 “全局” 的数据,例如当前经过身份验证的用户,主题或首选语言等。 例如,在下面的代码中,我们通过一个”theme” 属性(prop) 来手动创建 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, 我们可以避免通过中间元素传递 props:

// 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 。 请谨慎使用它,因为它使组件重用更加困难。

如果您只想避免在多个级别上传递一些 props ,那么 组件组合 component composition 通常比 Context 更简单。

例如,考虑一下,一个 Page 组件的 useravatarSize prop(属性) 要经过多少个层级传递 ,才能使深层嵌套的LinkAvatar 组件可以读取它们:

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

如果最终只有 Avatar 组件确实需要它,那么将 useravatarSize props(属性) 传递到多个层级可能会感到多余。同样令人讨厌的是,当 Avatar 组件从顶部需要更多的 props(属性) 时,你也必须在所有的中间层级的组件中添加它们。

没有 context 的情况下解决此问题的一种方法是 传递 Avatar 组件本身,这样中间层级的组件不需要知道 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 组件知道 LinkAvatar 组件需要使用 useravatarSize props(属性)。

在许多情况下,通过减少应用程序需要的 props(属性) 数量并对根组件进行更多控制,这种 反向控制 可以使代码更加清晰。 但是,在每种情况下,这都不是正确的选择:在树中移动更高的复杂性会使这些更高级别的组件更加复杂,并迫使较低级别的组件比您想要的更灵活。

对于一个组件,您不限于单个的子组件。 你可以传递多个子组件,甚至可以为子组件们分别设置多个“插槽”,如下所述

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 可能比替代方案更简单的常见示例包括管理当前本地设置,主题或数据缓存。

API

React.createContext

const MyContext = React.createContext(defaultValue);

创建一个 Context 对象对。当 React 渲染订阅这 个Context 对象的组件时,它将从组件树中匹配最接近的 Provider 中读取当前的 context 值。

defaultValue 参数 当 consumer(使用者) 在树中没有匹配的 Provider(提供则) 时使用它。这有助于在不封装它们的情况下对组件进行测试。注意:将 undefined 作为 Provider(提供者) 值传递不会导致 consumer(使用者) 组件使用 defaultValue

Context.Provider

<MyContext.Provider value={/* some value */}>

每个 Context 对象都附带一个 Provider React组件,允许 consumer(使用者) 组件 来 订阅 context 的改变。

接受一个 value 属性传递给使用组件,这个 consumer(使用者) 组件 为 Provider(提供者) 的后代组件 。一个 Provider 可以连接到许多 consumers 。Provider(提供者) 可以被嵌套以覆盖树中更深层次的值。

每当 Provider(提供者) 的 value 属性发生变化时,所有作为 Provider(提供者) 后代的 consumer(使用者) 组件 都将重新渲染。 从Provider 到其后代使用者的传播不受 shouldComponentUpdate 方法的约束,因此即使祖先组件退出更新,也会更新 consumer(使用者) 。

通过使用与Object.is 相同的算法,比较新旧值来确定更改。

注意

确定更改的方式 在将对象作为 value 传递时会导致一些问题:请参阅 注意事项

Class.contextType

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 属性分配由 React.createContext() 创建的 Context 对象。 这允许您使用this.context 使用该 Context 类型 的最近的当前值。 您可以在任何生命周期方法中引用它,包括 render 函数。

注意:

您只能使用这个 API 订阅单个上下文。 如果您需要订阅多个,请参阅 使用多个 Contexts

如果您使用的是实验性公共类字段语法,则可以使用 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>

一个可以订阅 context 变化的 React 组件。 这允许您订阅 函数式组件 中的 context 。

需要接收一个 函数作为子节点。 该函数接收当前 context 值并返回一个 React 节点。 传递给函数的 value 参数将等于组件树中上层这个 context 最接近的 Provider 的 value 属性。 如果上层没有提供这个 context 的 Provider ,value参数将等于传递给 createContext()defaultValue

注意

关于函数作为子节点的更多信息, 请看 render props

示例

动态 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);

从嵌套组件更新 context

我们通常需要从组件树中深层嵌套组件中更新 context。在这种情况下,您可以在 context 中向下传递一个函数,以允许 Consumer 更新 context :

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

使用多个 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
    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 (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

如果经常同时使用两个或多个 context 值,您可能需要考虑创建自己的渲染属性组件,同时提供两者。

告诫

因为 context 使用引用标识来确定何时重新渲染,当 Provider(提供者) 的父节点重新渲染时,有一些陷阱可能触发 Consumer(使用者) 无意渲染。例如,下面的代码将在每次 Provider(提供者) 重新渲染时,会重新渲染所有 Consumer(使用者) ,因为总是为 value 创建一个新对象:

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 。所有 16.x 版本都将支持旧的 API,但使用它的应用程序应该迁移到新版本。遗留 API 将在未来的主要 React 版本中删除。阅读遗留 context 文档