Рубріки: Теория

React Context Hook: что это, и как правильно его использовать?

Андрей Галадей

Одна из важнейших и наиболее сложных задач в разработке современных веб-приложений — управление их состоянием. Для этого есть уйма библиотек (Redux, например), часть из них создана поверх уже готовых решений. Однако есть интересный способ обойтись и вовсе без подключения внешних библиотек (React Hooks + context). Об этом мы сегодня и поговорим.

Содержание:
1. Что такое React Context?
2. Когда использовать контекст?
3. Перед тем, как вы начнете использовать контекст
4. API
4.1. React.createContext
4.2. Provider
4.3. Consumer
5. Примеры
5.1. Динамический контекст
5.2. Обновление контекста из вложенного компонента
5.3. Потребление множества контекстов
5.4. Доступ к контексту в методах жизненного цикла
5.5. Получение контекста старшим компонентом (HOC’ом)
5.6. Передача ссылок ref потребителям контекста
5.7. Возможные проблемы
5.8. Использование устаревшей версии API
6. Вывод

1. Что такое React Context?

В общем случае React Context — это один из способов передачи данных между компонентами приложения. Если говорить точнее, это передача глобальных props (пропсов), которые доступны в рамках всего приложения. А сами пропсы — это данные на входе React-компонентов, которые передаются от родительского компонента к дочернему в пределах древовидной структуры. Иначе говоря, контекст предназначен для передачи данных от одного вложенного компонента к другому.

Алгоритм можно визуализировать примерно так:

webformyself.com

Необходимое нам состояние расположено в компоненте App. Оно необходимо для компонентов UserProfile и UserDetails, для чего мы будет передавать его вниз по дереву, вот так:

tproger.ru

А вот так выглядит код.

Рассмотрим пример:

const ColorContext = React.createContext("yellow")
class App extends React.Component {
   render() {
       return (
           <ColorContext.Provider>
               <P />
           </ColorContext.Provider>
       )
   }
}
class P extends React.Component {
   render() {
       return (
           {this.context}
           <C />
       )
   }
}
class C extends React.Component {
   render() {
       return (
           <Sub-C />
       )
   }
}
class Sub-C extends React.Component {
   render() {
       <div>
           {this.context}
       </div>
   }
}

В коде есть три компонента: App -> P -> C -> Sub-C. Здесь App отображает P, P отображает C, а C отображает Sub-C, именно последний определяется как context с именем ColorContext. В состав ColorContext.Provider входит P, что позволяет ему получать доступ к данным, которые определены в ColorContext.

Таким образом, все дочерние компоненты P могут получать доступ к данным, которые определены ранее. Для этого выполняется {this.context}.

Чтобы React знал, что нужно передать context от родительского компонента к дочерним элементам, необходимо сначала определить два атрибута в родительском классе. Это следующие атрибуты:

  • childContextTypes,
  • getChildContext.

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

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

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

Так выглядит пример, где передается пропс theme:

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

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

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

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

//Создадим контекст для текущей
// UI-темы (со значением "light" по умолчанию).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Компонент Provider используется для передачи текущей
    // UI-темы вниз по дереву. Любой компонент может использовать
    // этот контекст и не важно, как глубоко он находится.
    // В этом примере мы передаем "dark" в качестве значения контекста.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// Компонент, который находится в середине,
// больше не должен явно передавать тему вниз.
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Определяем contextType, чтобы получить значение контекста.
  // React найдет (выше по дереву) ближайший Provider-компонент,
  // предоставляющий этот контекст, и использует его значение.
  // В этом примере значение UI-темы будет "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

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

3. Перед тем, как вы начнете использовать контекст

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

Одним из альтернативных способов является передача лишь части пропсов, это реализовано с помощью композиции компонентов. К примеру, вот так выглядит компонент Page, который передает пропсы user и avatarSize на несколько уровней вниз. Это нужно, чтобы пропсы 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. Тогда промежуточные компоненты не знают о пропсах 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}

Поступая таким образом, можно сильно упростить себе жизнь.

4. API

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

4.1. React.createContext

Создание контекста — самая простая операция. Код выглядит так:

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

Здесь создается пара поставщик (Provider) и потребитель (Consumer).

Логика работы выглядит так:

  1. При отрисовке потребителя контекста (Consumer) будет считываться текущее значение контекста.
  2. Его передает ближайший поставщик (Provider), который находится выше в иерархическом дереве.
  3. После считывания данные передаются потребителю.

Отметим, что аргумент defaultValue используется в тех случаях, когда в дереве нет поставщика. Это позволяет тестировать компонент изолированно.

4.2. Provider

Код поставщика выглядит так:

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

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

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

4.3. Consumer

Код потребителя выглядит так:

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

Этот компонент React подписывается на изменения контекста и получает данные от поставщика. Функция используется в качестве дочернего элемента, она принимает текущее значение контекста и на выходе возвращает узел React.

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

5. Примеры

Теперь рассмотрим некоторые примеры использования контекста в коде.

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

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

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

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

Код выглядит так:

// Контекст темы. Светлая тема по умолчанию.
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, который и будет представлять их.

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

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

5.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>;

5.7. Возможные проблемы

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

Пример такого проблемного кода:

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

Для обхода проблемы значение value переводим в состояние родителя.

Пример кода:

 class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'что-нибудь'}
    };
  }

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

Таким образом можно решить проблему с перерисовкой.

5.8. Использование устаревшей версии API

Отметим, что ранее в React использовалась экспериментальная версия API контекста. Она актуальна для ветки 16.x, но все приложения, использующие API, должны перейти на новую версию.

На начало 2022 года актуальной версией является 17.0.2.

6. Вывод

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

Помимо этого материала, наглядно посмотреть, что собой представляет контекст в React можно по видеоссылке ниже:

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023