React Context 全讲解

231 阅读4分钟

简介

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

API

React.createContext

const {Provider, Consumer} = React.createContext(defaultValue);

创建一个 { Provider, Consumer } 对。当 React 渲染 context Consumer 时,它将从组件树中匹配最接近的 Provider 中读取当前的 context 值。

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

Provider

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

React组件允许 Consumer(使用者) 订阅 context 的改变。

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

Consumer

<Consumer>
  {value => /* render something based on the context value */}
</Consumer>

一个可以订阅 context 变化的 React 组件。

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

只要 Provider 的 value 属性发生变化是,所有属于该 Provider 后代的 Consumers 就会重新渲染。 从 Provider 到它的后代 Consumers 的传播不受 shouldComponentUpdate 方法的约束, 所以即使当祖先组件退出更新时,后代 Consumer 也会被更新。

通过使用与Object.is相同的算法比较新值和旧值来确定 value 属性变化。

注意 当传递对象作为 value 时,在确定 value 属性是否变化时引发一些问题:Caveats

何时使用

Context 旨在共享一个组件树内可被视为 “全局” 的数据,例如当前经过身份验证的用户,主题或首选语言等。 例如,在下面的代码中,我们通过一个”theme” 属性(prop) 来手动创建 Button 组件的样式:

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

function Toolbar(props) {
  // 这个组件必须传递一个 "theme" prop 给 ThemeButton。
  // 如果应用中每一个按钮都需要定义“theme” 
  // 那我们是不是需要通过prop传递给所有的按钮?
  // 特别是对于嵌套层级深的组件,这个过程显然很繁琐。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

function ThemedButton(props) {
  return <Button theme={props.theme} />;
}

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

// Context 可以让我们深度传递一个值
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 通过 Context.Provider 这个值,无论组件嵌套的多深,都可以读到这个 theme
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 这个父组件不一定要有 "theme" prop
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  // 通过 Context.Consumer ThemedButton 可以直接读到这个值
  // 它会去查找最近的 Provider
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

从嵌套组件更新 context

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

theme-context.js

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

theme-toggler-button.js

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

function ThemeTogglerButton() {
  // 不仅拿到了 "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 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 值是一种相对常见的用例。 不是将 context 添加到每个生命周期方法中, 你只需将它作为 props , 然后像使用 props 一样使用它即可。

class Button extends React.Component {
  componentDidMount() {
    // ThemeContext value is this.props.theme
  }

  componentDidUpdate(prevProps, prevState) {
    // Previous ThemeContext value is prevProps.theme
    // New ThemeContext value is this.props.theme
  }

  render() {
    const {theme, children} = this.props;
    return (
      <button className={theme ? 'dark' : 'light'}>
        {children}
      </button>
    );
  }
}

export default props => (
  <ThemeContext.Consumer>
    {theme => <Button {...props} theme={theme} />}
  </ThemeContext.Consumer>
);

性能优化

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