React Context

Context 的目的是对于全局的需要多处使用的数据,不希望通过 prop 一层层传递下去,而是可以直接使用。

childContextTypes & getChildContext

该方案在 16.3 后不再是推荐方案。

React 最初提供的 context 方案是提供 context 数据的组件通过静态的 childContextType 来定义 context 中各字段的数据类型,通过 getChildContext 成员方法来提供实际的 context 数据,其中,返回的数据可以搭配 prop 和 state 来提供响应式的数据。而消费 context 数据的组件通过静态的 contextTypes 来指定需要消费的 context 字段的类型集合,然后即可通过 this.context[fieldName] 来使用。

注意,消费 context 的组件只需要定义需要的字段的类型即可,不需要是全部。同时,也不需要是提供 context 的组件的直接子组件。

import PropTypes from 'prop-types';

class Button extends React.Component {
  static contextTypes = {
    color: PropTypes.string
  };

  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>Delete</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  static childContextTypes = {
    color: PropTypes.string
  };

  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

Context Provider & Consumer

React 16.3 后提出新的 context 方案 React.createContext(anyInitValue),然后使用其提供的 ProviderConsumer 来使用。

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

export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

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

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

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

这种方案可以同时使用多个 context(provider 和 consumer 都可以作为普通标签来嵌套使用),并且可以使用 react-adopt 之类的库来解决 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 {
    state = {
        signedInUser: {
            name: 'Joe'
        },
        theme: 'dark'
    };

    render() {
        const { signedInUser, theme } = this.state;

        return (
            <ThemeContext.Provider value={theme}>
                <UserContext.Provider value={signedInUser}>
                    <Content />
                    <hr />
                    <Content2 />
                </UserContext.Provider>
            </ThemeContext.Provider>
        );
    }
}

function Content() {
    return (
        <ThemeContext.Consumer>
            {theme => (
                <UserContext.Consumer>
                    {user => (
                        <div>
                            <div>user: {user.name}</div>
                            <div>theme: {theme}</div>
                        </div>
                    )}
                </UserContext.Consumer>
            )}
        </ThemeContext.Consumer>
    );
}

// 组合来简化 consumer 的嵌套
const ComposeConsumer = adopt({
    theme: <ThemeContext.Consumer />,
    user: <UserContext.Consumer />
});

function Content2() {
    return (
        <ComposeConsumer>
            {({ theme, user }) => (
                <div>
                    <div>user: {user.name}</div>
                    <div>theme: {theme}</div>
                </div>
            )}
        </ComposeConsumer>
    );
}

contextType

Provider / Consumer 形式的 context,在 class 组件中使用时,初期只能在 render 系列的方法中才能使用。在 16.6.0 时添加了 contextType 来方便 class 组件在全生命周期内使用。

具体来说是通过静态的 contextType 指向创建成的 context,然后内部可以使用 this.context 来直接消费 context。

需要注意的是,这种形式只能消费一个 context,别的 context 还是需要通过 consumer 的形式来消费,并且 this.context 是 provider 提供的值。

const ThemeContext = React.createContext('light');

class App extends React.Component {
  state = {
    theme: 'dark'
  };

  render() {
    const { theme } = this.state;

    return (
      <ThemeContext.Provider value={theme}>
        <SingleCtx />
      </ThemeContext.Provider>
    );
  }
}

class SingleCtx extends React.Component {
  static contextType = ThemeContext;

  render() {
    return (
      <div>
        {this.context}
      </div>
    );
  }
}

Context Hook

useContext(createdContext) 是将在 16.7 引入的 context hook,便于在无状态组件中消费 createContext 创建的 context。使用语法上比 Consumer 的简单很多,能直接使用普通的语法而不需要使用 render prop 的语法。

function App() {
  const theme = useContext(ThemeContext)
  const language = useContext(LanguageContext)
  return <div>{theme} and {language}</div>
}

参考