3.9 Контекст

Контекст обеспечивает способ передачи данных через дерево компонентов без необходимости передавать свойства вручную на каждом уровне.

В типичном приложении React данные передаются сверху вниз (от родителя к потомку) через свойства props. Однако это может оказаться громоздким для определенных типов свойств (тема UI; предпочтения, связанные с локалью), которые требуются для многих компонентов в приложении. Контекст предоставляет способ совместного использования таких значений между компонентами без необходимости явно передавать свойство через каждый уровень дерева.


3.9.1 Когда следует использовать контекст?

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


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

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

  function ThemedButton(props) {
    return <Button theme={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(props) {
    return (
      <div>
        <ThemedButton />
      </div>
    );
  }

  function ThemedButton(props) {
    // Используйте Consumer, чтобы считать текущий контекст темы.
    // React будет искать выше ближайший поставщик (Provider) темы и использует его значение.
    // В данном примере текущая тема имеет значение "dark".
    return (
      <ThemeContext.Consumer>
        {theme => <Button {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
  


Внимание!

Не используйте контекст, чтобы избежать передачи свойств на несколько уровней ниже. Реальная необходимость возникает в случаях, когда одни и те же данные должны быть доступны во многих компонентах на разных уровнях.


3.9.2 API


3.9.2.1 React.createContext.


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

Создает пару потребитель и поставщик(провайдер): {Provider, Consumer}. Когда React будет отрисовывать потребителя контекста Consumer, он считает текущее значение контекста из ближайшего соответствующего поставщика Provider выше в дереве иерархии.

Аргумент defaultValue используется при отрисовке потребителя контекста, не имеющего соответствующего поставщика выше него в дереве. Это может быть полезно для тестирования компонентов изолированно, без их обертывания.


3.9.2.2 Provider.


Код
    
  <Provider value={/* some value */}>
  

Компонент React, который позволяет потребителям подписываться на изменения контекста.

Принимает свойство value, которое должно быть передано потребителям, которые являются потомками данного провайдера. Один провайдер может быть связан со многими потребителями. Провайдеры могут быть вложенными, чтобы переопределять значения глубже в дереве.


3.9.2.3 Consumer.


Код
    
  <Consumer>
    {value => /* отрисовывает что-то, что основано на значении контекста */}
  </Consumer>
  

Компонент React, который подписывается на изменения контекста.

Требует функцию в качестве дочернего элемента. Функция получает текущее значение контекста и возвращает узел React. Аргумент value, переданный функции, будет равен свойству value ближайшего поставщика для этого контекста выше в дереве. Если для данного контекста нет провайдера, аргумент value будет равен значению defaultValue, которое было передано в createContext().



Все потребители перерисовываются всякий раз при изменении значения поставщика. Изменения определяются путем сравнения старых и новых значений с использованием того же алгоритма, что и в Object.is. (Это может вызвать некоторые проблемы при передаче объектов в качестве value: см. раздел Предостережения .)


3.9.3 Примеры.


3.9.3.1 Динамический контекст

Более сложный пример с динамическими значениями для темы:

theme-context.js


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

  export const ThemeContext = React.createContext(
    themes.dark // значеине по умолчанию
  );
  

themed-button.js


Код
    
  import {ThemeContext} from './theme-context';
  
  function ThemedButton(props) {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <button
            {...props}
            style={{backgroundColor: theme.background}}
          />
        )}
      </ThemeContext.Consumer>
    );
  }
  
  export default ThemedButton;
  

app.js


Код
    
  import {ThemeContext, themes} from './theme-context';
  import ThemedButton from './themed-button';
  
  // Промежуточный компонент, который использует 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 => ({
          const { light, dark } = themes
          theme: state.theme === dark ? light : dark
        }));
      };
    }
  
    render() {
      // Кнопка ThemedButton внутри ThemeProvider
      // использует тему из состояния, в то время как снаружи
      // использует тему по умолчанию: dark
      return (
        <Page>
          <ThemeContext.Provider value={this.state.theme}>
            <Toolbar changeTheme={this.toggleTheme} />
          </ThemeContext.Provider>
          <Section>
            <ThemedButton />
          </Section>
        </Page>
      );
    }
  }
  
  ReactDOM.render(<App />, document.root);
  


3.9.3.2 Обновление контекста из вложенного компонента

Часто необходимо обновить контекст из компонента, который глубоко вложен в дерево компонентов. В этом случае вы можете передать функцию через контекст, чтобы позволить потребителям обновлять контекст:

theme-context.js


Код
    
  // Убедитесь, что форма значения по умолчанию, переданная в
  // createContext соответствует форме, которую ожидают потребители!
  export const ThemeContext = React.createContext({
    theme: themes.dark,
    toggleTheme: () => {},
  });
  

theme-toggler-button.js


Код
    
  import {ThemeContext} from './theme-context';
  
  function ThemeTogglerButton() {
    // Компонент ThemeTogglerButton принимает не только тему,
    // но и функцию 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,
        }));
      };
  
      // Состояние также содержит обновляющую функцию, поэтому она
      // будет передана в поставщик контекста
      this.state = {
        theme: themes.light,
        toggleTheme: this.toggleTheme
      };
    }
  
    render() {
      // Состояние целиком передается в поставщик
      return (
        <ThemeContext.Provider value={this.state}>
          <Content />
        </ThemeContext.Provider>
      );
    }
  }
  
  function Content() {
    return (
      <div>
        <ThemeTogglerButton />
      </div>
    );
  }
  
  ReactDOM.render(<App />, document.root);
  


3.9.3.3 Потребление множества контекстов

Чтобы поддерживать перерисовку контекста быстрой, React должен сделать каждого потребителя контекста отдельным узлом в дереве.


Код
    
  // Контекст темы. Светлая тема по умолчанию.
  const ThemeContext = React.createContext('light');
  
  // Контекст Signed-in  пользователя
  const UserContext = React.createContext();
  
  class App extends React.Component {
    render() {
      const {signedInUser, theme} = this.props;
  
      // Компонент приложения, который предоставляет начальные значения контекста
      return (
        <ThemeContext.Provider value={theme}>
          <UserContext.Provider value={signedInUser}>
            <Layout />
          </UserContext.Provider>
        </ThemeContext.Provider>
      );
    }
  }
  
  function Layout() {
    return (
      <div>
        <Sidebar />
        <Content />
      </div>
    );
  }
  
  // Компонент может потреблять множество контекстов
  function Content() {
    return (
      <ThemeContext.Consumer>
        {theme => (
          <UserContext.Consumer>
            {user => (
              <ProfilePage user={user} theme={theme} />
            )}
          </UserContext.Consumer>
        )}
      </ThemeContext.Consumer>
    );
  }
  

Если два и более значения контекста часто используются вместе, вы можете рассмотреть возможность создания вашего собственного render prop компонента, который будет их предоставлять.




3.9.3.4 Доступ к контексту в методах жизненного цикла

Доступ к значениям из контекста в методах жизненного цикла является довольно частой ситуацией. Вместо добавления контекста в каждый метод жизненного цикла вам просто нужно передать его как свойство, а затем работать с ним так же, как и с обычным свойством.


Код
    
  class Button extends React.Component {
    componentDidMount() {
      // this.props.theme - текущее значение контекста
    }
  
    componentDidUpdate(prevProps, prevState) {
      // prevProps.theme - предыдущее значение контекста
      // 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>
  );
  


3.9.3.5 Потребление контекста старшим компонентом (HOC-ом)

Некоторые типы контекстов могут потребляться многими компонентами (например, тема или локализация). Может быть утомительно явно обертывать каждую зависимость с помощью элемента <Context.Consumer>. Старший компонент может помочь избежать этого.

Например, компонент кнопки может потреблять контекст темы следующим образом:


Код
    
  const ThemeContext = React.createContext('light');
  
  function ThemedButton(props) {
    return (
      <ThemeContext.Consumer>
            {theme => <button className={theme} {...props} />}
      </ThemeContext.Consumer>
    );
  }
  

Для нескольких компонентов это выглядит хорошо, но что, если бы мы захотели использовать контекст темы во множестве мест?

Мы могли бы создать старший компонент с withTheme:


Код
    
  const ThemeContext = React.createContext('light');
  
  // Эта функция принимает компонент...
  export function withTheme(Component) {
    // ... возвращает другой компонент...
    return function ThemedComponent(props) {
      // ... и отрисовывает обернутый компонент с темой из контекста!
      // Обратите внимание, что мы с таким же успехом можем передавать
      // любое дополнительное свойство
      return (
        <ThemeContext.Consumer>
          {theme => <Component {...props} theme={theme} />}
        </ThemeContext.Consumer>
      );
    };
  }
  

Теперь любой компонент, который зависит от контекста темы, может легко подписаться на него с помощью созданной нами функции withTheme:


Код
    
  function Button({theme, ...rest}) {
    return <button className={theme} {...rest} />;
  }
  
  const ThemedButton = withTheme(Button);
  


3.9.3.6 Передача ссылок ref в потребители контекста

Одна из проблем с render prop API-интерфейсом заключается в том, что ссылки ref не передается автоматически обернутым элементам. Чтобы обойти это, используйте React.forwardRef:

fancy-button.js


Код
    
  class FancyButton extends React.Component {
    focus() {
      // ...
    }
  
    // ...
  }
  
  // Используйте контекст, чтобы передать текущую "theme" в FancyButton.
  // Используйте forwardRef, чтобы с тем же успехом передавать ссылки ref в FancyButton.
  export default React.forwardRef((props, ref) => (
    <ThemeContext.Consumer>
      {theme => (
        <FancyButton {...props} theme={theme} ref={ref} />
      )}
    </ThemeContext.Consumer>
  ));
  

app.js


Код
    
  import FancyButton from './fancy-button';
  
  const ref = React.createRef();
  
  // Наша ссылка ref будет указывать на компонент FancyButton,
  // а не на ThemeContext.Consumer, который его оборачивает.
  // Это означает, что мы можем вызывать FancyButton методы как ref.current.focus()
  <FancyButton ref={ref} onClick={handleClick}>
    Click me!
  </FancyButton>;
  


3.9.4 Предостережения

Поскольку контекст использует ссылочную идентификацию, чтобы определить, когда нужно проводить перерисовку, существуют некоторые "подводные камни", которые могут вызвать непреднамеренные отрисовки в потребителях, когда перерисовывается родитель поставщика. Например, приведенный ниже код будет повторно отрисовывать всех потребителей каждый раз, когда перерисовывается поставщик, потому что для value всегда создается новый объект:


Код
    
  class App extends React.Component {
    render() {
      return (
        <Provider value={{something: 'что-нибудь'}}>
          <Toolbar />
        </Provider>
        );
    }
  }
  

Чтобы обойти это, поднимите значение в состояние родителя:


Код
    
  class App extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        value: {something: 'что-нибудь'}
      };
    }
  
    render() {
      return (
       <Provider value={this.state.value}>
          <Toolbar />
        </Provider>
      );
    }
  }
  


3.9.5 Устаревший API


Внимание!

Ранее React предоставлял экспериментальный API контекста. Устаревший API будет поддерживаться во всех релизах 16.x, но приложения, использующие его, должны мигрировать на новую версию. Он будет удален в будущей major-версии React. Ознакомиться с устаревшим API контекста вы сможете здесь.