Unetway

ReactJS - Context (Контекст)

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

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

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

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

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

function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

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

Используя контекст, мы можем избежать передачи реквизита через промежуточные элементы:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  // Use a Consumer to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  return (
    <ThemeContext.Consumer>
      {theme => <Button {...props} theme={theme} />}
    </ThemeContext.Consumer>
  );
}

Заметка

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

API

React.createContext

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

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

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

Provider

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

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

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

Consumer

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

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

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

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

Примеры

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

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

theme-context.js

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

export const ThemeContext = React.createContext(
  themes.dark // default value
);

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

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

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

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

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

Использование нескольких контекстов

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

// Theme context, default to light theme
const ThemeContext = React.createContext('light');

// Signed-in user context
const UserContext = React.createContext();

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

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

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

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

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

Контекст потребления с помощью 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');

// This function takes a component...
export function withTheme(Component) {
  // ...and returns another component...
  return function ThemedComponent(props) {
    // ... and renders the wrapped component with the context theme!
    // Notice that we pass through any additional props as well
    return (
      <ThemeContext.Consumer>
        {theme => <Component {...props} theme={theme} />}
      </ThemeContext.Consumer>
    );
  };
}

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

function Button({theme, ...rest}) {
  return <button className={theme} {...rest} />;
}

const ThemedButton = withTheme(Button);

Пересылка ссылок на контекстных потребителей

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

Расшивка button.js

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// Use context to pass the current "theme" to FancyButton.
// Use forwardRef to pass refs to FancyButton as well.
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();

// Our ref will point to the FancyButton component,
// And not the ThemeContext.Consumer that wraps it.
// This means we can call FancyButton methods like ref.current.focus()
<FancyButton ref={ref} onClick={handleClick}>
  Click me!
</FancyButton>;

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

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

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

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

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

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