Контекст

Контекст забезпечує спосіб передавати дані через дерево компонентів без необхідності передавати пропси вручну на кожному рівні.

У типовому додатку React дані передаються зверху вниз (від батьківської до дочірньої компоненти) через пропси, але це може бути громіздким для певних типів реквізитів (наприклад, налаштування локалі, тема інтерфейсу користувача), які потрібні багатьом компонентам програми. Контекст надає спосіб обмінюватися значеннями, подібними до цих, між компонентами без необхідності явно передавати властивість через кожен рівень дерева.

Коли використовувати контекст

Контекст призначений для обміну даними, які можна вважати «глобальними» для дерева компонентів React, таких як поточний автентифікований користувач, тема чи бажана мова. Наприклад, у наведеному нижче коді ми вручну прокидаємо проп theme, щоб стилізувати компонент Button:

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

function Toolbar(props) {
  // Компонент Toolbar має приймати додатковий проп "theme"  // та передавати його в ThemedButton. Це може стати проблемою  // якщо кожна окрема кнопка в додатку повинна знати тему  // оскільки його потрібно було б пропустити через усі компоненти.  return (
    <div>
      <ThemedButton theme={props.theme} />    </div>
  );
}

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

Використовуючи контекст, ми можемо уникнути проходження атрибутів через проміжні елементи:

// Контекст дозволяє передати значення глибоко в дерево компонентів// без явного проходження через кожен компонент.// Створення контексту для поточної теми (з "light" за замовчуванням).const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // Використовуйте компонент Provider для передачі поточної теми в дерево нижче.    // Будь-який компонент може прочитати його, незалежно від того, наскільки він глибокий.    // У цьому прикладі ми передаємо "dark" як поточне значення.    return (
      <ThemeContext.Provider value="dark">        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// Компонент посередині не обов’язково// передавати тему явно.function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Призначте contextType для читання контексту поточної теми.  // React знайде найближчий постачальник теми вище та використає його значення.  // У цьому прикладі поточна тема "dark".  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;  }
}

Перед використанням контексту

Контекст в основному використовується, коли деякі дані повинні бути доступні багатьом компонентам на різних рівнях вкладеності. Застосовуйте його економно, оскільки це ускладнює повторне використання компонентів.

Якщо ви тільки хочете уникнути проходження деяких реквізитів через багато рівнів, композиція компонентів часто є простішим рішенням, ніж контекст.

Наприклад, розглянемо компонент Page, який передає властивість user і avatarSize на кілька рівнів нижче, щоб глибоко вкладені компоненти Link і Avatar могли прочитати його:

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

Може здатися зайвим передавати властивості user і avatarSize через багато рівнів, якщо зрештою вони дійсно потрібні лише компоненту Avatar. Також дратує те, що щоразу, коли компонент Avatar потребує додаткових реквізитів зверху, ви також повинні додавати їх на всіх проміжних рівнях.

Один із способів вирішити цю проблему без контексту — це передавати сам компонент Avatar щоб проміжним компонентам не потрібно було знати про властивості 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 має знати про використання user і avatarSize компонентами Link і Avatar.

Ця інверсія керування може зробити ваш код чистішим у багатьох випадках, зменшивши кількість реквізитів, які потрібно пропустити через вашу програму, і надавши більше контролю кореневим компонентам. Однак це не завжди правильний вибір: переміщення більшої складності вище в дереві робить компоненти вищого рівня складнішими та змушує компоненти нижчого рівня бути більш гнучкими, ніж вам хочеться.

Ви не обмежені одним дочірнім компонентом. Ви можете передати кілька дочірніх компонентів або навіть мати кілька окремих “слотів” для дочірніх компонентів, як описано тут:

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

Ціого паттерну достатньо для багатьох випадків, коли потрібно відділити дочірній компонент від найближчих батьків. Ви можете піти ще далі за допомогою рендер-пропси якщо дочірня компонента потребує взаємодії з батьківською перед рендерингом.

Однак іноді ті самі дані повинні бути доступні багатьом компонентам у дереві та на різних рівнях вкладеності. Контекст дозволяє вам “транслювати” такі дані та зміни до них усім компонентам нижче. Поширені приклади, коли використання контексту може бути простішим, ніж альтернативи, включають керування поточною мовою, темою або кеш-пам’яттю даних.

API

React.createContext

const MyContext = React.createContext(defaultValue);

Створює об’єкт Context. Коли React відтворює компонент, який підписується на цей об’єкт Context, він читатиме поточне значення контексту з найближчого відповідного Provider над ним у дереві.

Аргумент defaultValue використовується тільки, коли компонент не має відповідного провайдера над ним у дереві. Це може бути корисним для тестування компонентів ізольовано без їх упаковки. Примітка: передача undefined як значення провайдера не призводить до використання споживаючими компонентами defaultValue.

Context.Provider

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

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.

Accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant consumers (including .contextType and useContext) is not subject to the shouldComponentUpdate method, so the consumer is updated even when an ancestor component skips an update.

Changes are determined by comparing the new and old values using the same algorithm as Object.is.

Note

The way changes are determined can cause some issues when passing objects as value: see Caveats.

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;

The contextType property on a class can be assigned a Context object created by React.createContext(). This lets you consume the nearest current value of that Context type using this.context. You can reference this in any of the lifecycle methods including the render function.

Note:

You can only subscribe to a single context using this API. If you need to read more than one see Consuming Multiple Contexts.

If you are using the experimental public class fields syntax, you can use a static class field to initialize your 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>

A React component that subscribes to context changes. This lets you subscribe to a context within a function component.

Requires a function as a child. The function receives the current context value and returns a React node. The value argument passed to the function will be equal to the value prop of the closest Provider for this context above in the tree. If there is no Provider for this context above, the value argument will be equal to the defaultValue that was passed to createContext().

Note

For more information about the ‘function as a child’ pattern, see render props.

Context.displayName

Context object accepts a displayName string property. React DevTools uses this string to determine what to display for the context.

For example, the following component will appear as MyDisplayName in the DevTools:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

Examples

Dynamic Context

A more complex example with dynamic values for the 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';

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

Updating Context from a Nested Component

It is often necessary to update the context from a component that is nested somewhere deeply in the component tree. In this case you can pass a function down through the context to allow consumers to update the 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);

Consuming Multiple Contexts

To keep context re-rendering fast, React needs to make each context consumer a separate node in the tree.

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

If two or more context values are often used together, you might want to consider creating your own render prop component that provides both.

Caveats

Because context uses reference identity to determine when to re-render, there are some gotchas that could trigger unintentional renders in consumers when a provider’s parent re-renders. For example, the code below will re-render all consumers every time the Provider re-renders because a new object is always created for value:

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

To get around this, lift the value into the parent’s state:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},    };
  }

  render() {
    return (
      <Provider value={this.state.value}>        <Toolbar />
      </Provider>
    );
  }
}

Legacy API

Note

React previously shipped with an experimental context API. The old API will be supported in all 16.x releases, but applications using it should migrate to the new version. The legacy API will be removed in a future major React version. Read the legacy context docs here.