Spec-Zone.ru › React 17

Контекст

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

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

  • Когда использовать контекст
  • Перед использованием контекста
  • API

    • React.createContext
    • Context.Provider
    • Class.contextType
    • Context.Consumer
    • Context.displayName
  • Примеры

    • Динамический контекст
    • Обновление контекста из вложенного компонента
    • Потребление нескольких контекстов
  • Ограничения
  • API предыдущей версии

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

Контекст предназначен для обмена данными, которые можно считать «глобальными» для дерева React-компонентов, такими как текущий аутентифицированный пользователь, тема или предпочтительный язык. Например, в коде ниже мы вручную передаём prop «theme», чтобы стилизовать компонент 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>
  );
}

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

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

// 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() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType 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".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

Перед использованием контекста

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

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

Например, рассмотрим компонент Page, который передаёт prop user и prop avatarSize на несколько уровней вниз, чтобы глубоко вложенные компоненты Link и Avatar могли их прочитать:

<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... which renders ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... which renders ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

Может показаться избыточным передавать props user и avatarSize через много уровней, если в конечном итоге только компонент Avatar действительно нуждается в них. Также неудобно, что всякий раз, когда компоненту Avatar нужны дополнительные props сверху, вы должны добавлять их на все промежуточные уровни.

Один из способов решения этой проблемы без контекста — передача компонента Avatar, чтобы промежуточные компоненты не нуждались в знании о props 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} />;
}

// Now, we have:
<Page user={user} avatarSize={avatarSize} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}

С этим изменением только верхний компонент Page должен знать о том, как компоненты Link и Avatar используют user и avatarSize.

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

Вы не ограничены одним дочерним компонентом. Вы можете передать несколько дочерних элементов или даже иметь несколько отдельных «слотов» для дочерних элементов, как описано здесь:

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

Этот шаблон достаточен для многих случаев, когда вам нужно отделить дочерний элемент от его непосредственных родителей. Вы можете пойти ещё дальше с render props, если дочерний компонент нуждается в общении с родительским компонентом перед рендерингом.

Однако иногда одни и те же данные должны быть доступны многим компонентам в дереве, и на разных уровнях вложенности. Контекст позволяет «распространять» такие данные и изменения в них всем компонентам ниже. Типичные примеры, в которых использование контекста может быть проще, чем альтернативы, включают управление текущим языком, темой или кэшем данных.

API

React.createContext

const MyContext = React.createContext(defaultValue);

Создаёт объект Context. Когда React рендерит компонент, который подписывается на этот объект Context, он считывает текущее значение контекста из ближайшего соответствующего Provider выше него в дереве.

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

Context.Provider

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

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

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

Все потребители, которые являются потомками Provider, будут перерендериться всякий раз, когда prop value Provider изменится. Распространение от Provider к его потомкам-потребителям (включая .contextType и useContext) не зависит от метода shouldComponentUpdate, поэтому потребитель обновляется даже когда родительский компонент пропускает обновление.

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

Примечание

Способ определения изменений может вызвать некоторые проблемы при передаче объектов в качестве value: см. Ограничения.

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;

Свойство contextType класса может быть присвоено объекту Context, созданному с помощью React.createContext(). Использование этого свойства позволяет потреблять ближайшее текущее значение этого типа Context с помощью this.context. Вы можете обратиться к нему в любом из методов жизненного цикла, включая функцию рендеринга.

Примечание:

Вы можете подписаться только на один контекст с помощью этого API. Если вам нужно прочитать больше, см. Потребление нескольких контекстов.

Если вы используете экспериментальный синтаксис публичных свойств класса, вы можете использовать статическое свойство класса для инициализации вашего 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>

React-компонент, который подписывается на изменения контекста. Использование этого компонента позволяет подписаться на контекст внутри функционального компонента.

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

Примечание

Для получения дополнительной информации о шаблоне «функция в качестве дочернего элемента» см. render props.

Context.displayName

Объект контекста принимает строковое свойство displayName. React DevTools использует эту строку для определения того, что отображать для контекста.

Например, следующий компонент будет отображаться как MyDisplayName в DevTools:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" in DevTools
<MyContext.Consumer> // "MyDisplayName.Consumer" in DevTools

Примеры

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

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

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

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

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

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

Потребление нескольких контекстов

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

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

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

Ограничения

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

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

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

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

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

API предыдущей версии

Примечание

React ранее поставлялся с экспериментальным API контекста. Старый API будет поддерживаться во всех выпусках 16.x, но приложения, использующие его, должны мигрировать на новую версию. Старый API будет удалён в будущей основной версии React. Прочитайте документацию по контексту предыдущей версии здесь.

Полезен ли этот раздел?

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://17.reactjs.org/docs/context.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API