• Context
    • 何时使用 Context
    • 使用 Context 之前的考虑
    • API
      • React.createContext
      • Context.Provider
      • Class.contextType
      • Context.Consumer
    • 示例
      • 动态 Context
      • 在嵌套组件中更新 Context
      • 消费多个 Context
    • 注意事项
    • 过时的 API

    Context

    Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

    在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

    • 何时使用 Context
    • 使用 Context 之前的考虑
    • API

      • React.createContext
      • Context.Provider
      • Class.contextType
      • Context.Consumer
    • 示例

      • 动态 Context
      • 在嵌套组件中更新 Context
      • 使用多个 Context
    • 注意事项
    • 废弃的 API

    何时使用 Context

    Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:

    1. class App extends React.Component {
    2. render() {
    3. return <Toolbar theme="dark" />;
    4. }
    5. }
    6. function Toolbar(props) {
    7. // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
    8. // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
    9. // 因为必须将这个值层层传递所有组件。
    10. return (
    11. <div>
    12. <ThemedButton theme={props.theme} />
    13. </div>
    14. );
    15. }
    16. class ThemedButton extends React.Component {
    17. render() {
    18. return <Button theme={this.props.theme} />;
    19. }
    20. }

    使用 context, 我们可以避免通过中间元素传递 props:

    1. // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
    2. // 为当前的 theme 创建一个 context(“light”为默认值)。
    3. const ThemeContext = React.createContext('light');
    4. class App extends React.Component {
    5. render() {
    6. // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    7. // 无论多深,任何组件都能读取这个值。
    8. // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    9. return (
    10. <ThemeContext.Provider value="dark">
    11. <Toolbar />
    12. </ThemeContext.Provider>
    13. );
    14. }
    15. }
    16. // 中间的组件再也不必指明往下传递 theme 了。
    17. function Toolbar(props) {
    18. return (
    19. <div>
    20. <ThemedButton />
    21. </div>
    22. );
    23. }
    24. class ThemedButton extends React.Component {
    25. // 指定 contextType 读取当前的 theme context。
    26. // React 会往上找到最近的 theme Provider,然后使用它的值。
    27. // 在这个例子中,当前的 theme 值为 “dark”。
    28. static contextType = ThemeContext;
    29. render() {
    30. return <Button theme={this.context} />;
    31. }
    32. }

    使用 Context 之前的考虑

    Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

    如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。

    比如,考虑这样一个 Page 组件,它层层向下传递 useravatarSize 属性,从而深度嵌套的 LinkAvatar 组件可以读取到这些属性:

    1. <Page user={user} avatarSize={avatarSize} />
    2. // ... 渲染出 ...
    3. <PageLayout user={user} avatarSize={avatarSize} />
    4. // ... 渲染出 ...
    5. <NavigationBar user={user} avatarSize={avatarSize} />
    6. // ... 渲染出 ...
    7. <Link href={user.permalink}>
    8. <Avatar user={user} size={avatarSize} />
    9. </Link>

    如果在最后只有 Avatar 组件真的需要 useravatarSize,那么层层传递这两个 props 就显得非常冗余。而且一旦 Avatar 组件需要更多从来自顶层组件的 props,你还得在中间层级一个一个加上去,这将会变得非常麻烦。

    一种无需 context 的解决方案是将 Avatar 组件自身传递下去,因而中间组件无需知道 user 或者 avatarSize 等 props:

    1. function Page(props) {
    2. const user = props.user;
    3. const userLink = (
    4. <Link href={user.permalink}>
    5. <Avatar user={user} size={props.avatarSize} />
    6. </Link>
    7. );
    8. return <PageLayout userLink={userLink} />;
    9. }
    10. // 现在,我们有这样的组件:
    11. <Page user={user} avatarSize={avatarSize} />
    12. // ... 渲染出 ...
    13. <PageLayout userLink={...} />
    14. // ... 渲染出 ...
    15. <NavigationBar userLink={...} />
    16. // ... 渲染出 ...
    17. {props.userLink}

    这种变化下,只有最顶部的 Page 组件需要知道 LinkAvatar 组件是如何使用 useravatarSize 的。

    这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。

    而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”,正如这里的文档所列举的

    1. function Page(props) {
    2. const user = props.user;
    3. const content = <Feed user={user} />;
    4. const topBar = (
    5. <NavigationBar>
    6. <Link href={user.permalink}>
    7. <Avatar user={user} size={props.avatarSize} />
    8. </Link>
    9. </NavigationBar>
    10. );
    11. return (
    12. <PageLayout
    13. topBar={topBar}
    14. content={content}
    15. />
    16. );
    17. }

    这种模式足够覆盖很多场景了,在这些场景下你需要将子组件和直接关联的父组件解耦。如果子组件需要在渲染前和父组件进行一些交流,你可以进一步使用 render props。

    但是,有的时候在组件树中很多不同层级的组件需要访问同样的一批数据。Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据,这比替代方案要简单的多。

    API

    React.createContext

    1. const MyContext = React.createContext(defaultValue);

    创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

    只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 时,消费组件的 defaultValue 不会生效。

    Context.Provider

    1. <MyContext.Provider value={/* 某个值 */}>

    每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

    Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

    当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

    通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

    注意

    当传递对象给 value 时,检测变化的方式会导致一些问题:详见注意事项。

    Class.contextType

    1. class MyClass extends React.Component {
    2. componentDidMount() {
    3. let value = this.context;
    4. /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
    5. }
    6. componentDidUpdate() {
    7. let value = this.context;
    8. /* ... */
    9. }
    10. componentWillUnmount() {
    11. let value = this.context;
    12. /* ... */
    13. }
    14. render() {
    15. let value = this.context;
    16. /* 基于 MyContext 组件的值进行渲染 */
    17. }
    18. }
    19. MyClass.contextType = MyContext;

    挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。

    注意:

    你只通过该 API 订阅单一 context。如果你想订阅多个,阅读使用多个 Context 章节

    如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType

    1. class MyClass extends React.Component {
    2. static contextType = MyContext;
    3. render() {
    4. let value = this.context;
    5. /* 基于这个值进行渲染工作 */
    6. }
    7. }

    Context.Consumer

    1. <MyContext.Consumer>
    2. {value => /* 基于 context 值进行渲染*/}
    3. </MyContext.Consumer>

    这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。

    这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue

    注意

    想要了解更多关于“函数作为子元素(function as a child)”模式,详见 render props。

    示例

    动态 Context

    对于上面的 theme 例子,使用动态值(dynamic values)后更复杂的用法:

    theme-context.js

    1. export const themes = {
    2. light: {
    3. foreground: '#000000',
    4. background: '#eeeeee',
    5. },
    6. dark: {
    7. foreground: '#ffffff',
    8. background: '#222222',
    9. },
    10. };
    11. export const ThemeContext = React.createContext(
    12. themes.dark // 默认值
    13. );

    themed-button.js

    1. import {ThemeContext} from './theme-context';
    2. class ThemedButton extends React.Component {
    3. render() {
    4. let props = this.props;
    5. let theme = this.context;
    6. return (
    7. <button
    8. {...props}
    9. style={{backgroundColor: theme.background}}
    10. />
    11. );
    12. }
    13. }
    14. ThemedButton.contextType = ThemeContext;
    15. export default ThemedButton;

    app.js

    1. import {ThemeContext, themes} from './theme-context';
    2. import ThemedButton from './themed-button';
    3. // 一个使用 ThemedButton 的中间组件
    4. function Toolbar(props) {
    5. return (
    6. <ThemedButton onClick={props.changeTheme}>
    7. Change Theme
    8. </ThemedButton>
    9. );
    10. }
    11. class App extends React.Component {
    12. constructor(props) {
    13. super(props);
    14. this.state = {
    15. theme: themes.light,
    16. };
    17. this.toggleTheme = () => {
    18. this.setState(state => ({
    19. theme:
    20. state.theme === themes.dark
    21. ? themes.light
    22. : themes.dark,
    23. }));
    24. };
    25. }
    26. render() {
    27. // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,
    28. // 而外部的组件使用默认的 theme 值
    29. return (
    30. <Page>
    31. <ThemeContext.Provider value={this.state.theme}>
    32. <Toolbar changeTheme={this.toggleTheme} />
    33. </ThemeContext.Provider>
    34. <Section>
    35. <ThemedButton />
    36. </Section>
    37. </Page>
    38. );
    39. }
    40. }
    41. ReactDOM.render(<App />, document.root);

    在嵌套组件中更新 Context

    从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context:

    theme-context.js

    1. // 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
    2. export const ThemeContext = React.createContext({
    3. theme: themes.dark,
    4. toggleTheme: () => {},
    5. });

    theme-toggler-button.js

    1. import {ThemeContext} from './theme-context';
    2. function ThemeTogglerButton() {
    3. // Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
    4. return (
    5. <ThemeContext.Consumer>
    6. {({theme, toggleTheme}) => (
    7. <button
    8. onClick={toggleTheme}
    9. style={{backgroundColor: theme.background}}>
    10. Toggle Theme
    11. </button>
    12. )}
    13. </ThemeContext.Consumer>
    14. );
    15. }
    16. 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);

    消费多个 Context

    为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

    // Theme context,默认的 theme 是 “light” 值
    const ThemeContext = React.createContext('light');
    
    // 用户登录 context
    const UserContext = React.createContext({
      name: 'Guest',
    });
    
    class App extends React.Component {
      render() {
        const {signedInUser, theme} = this.props;
    
        // 提供初始 context 值的 App 组件
        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 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 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 运行,旧的 API 将会在所有 16.x 版本中得到支持,但用到它的应用应该迁移到新版本。过时的 API 将在未来的 React 版本中被移除。阅读过时的 context 文档了解更多。