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

React Context: полное руководство по использованию

Сергей Почекутов

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

1. Когда использовать Context

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

Главное условие — данные должны быть размещены в контексте. Context — это не полноценная система управления состоянием, как Redux. Это инструмент, который упрощает использование данных в приложении (об одном из его аспектов можно почитать в нашей статье про React Context Hook).

2. Какие проблемы решает React Context

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

Вот простой пример:

export default function App({ theme }) {
  return (
    <>
      <Header theme={theme} />
      <Main theme={theme} />
      <Sidebar theme={theme} />
      <Footer theme={theme} />
    </>
  );
}

function Header({ theme }) {
  return (
    <>
      <User theme={theme} />
      <Login theme={theme} />
      <Menu theme={theme} />
    </>
  );
}

Здесь мы передаем свойство theme через все компоненты приложения. При этом они не нужны, например, Header. Он просто выступает в качестве передатчика для дочернего компонента. Было бы намного лучше, если бы компоненты User, Login и Menu получали свойства напрямую. Эту проблему участия «лишних» компонентов и решает React Context. Изучить особенности его работы можно на курсах наших партнеров, школы Mate Academy и Hillel.

3. API

React.createContext

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

Пример:

const NewContext = React.createContext({ color: 'black' });

Компоненты обычно оборачивают в Provider, чтобы они получали значение контекста. Но если по дереву над компонентом нет Provider, то он получает значение от стандартного аргумента в методе createContext. В данном случае это { color: 'black' }.

Context.Provider

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

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

Пример:

<Provider value={{color: 'blue'}}>
  {children}
</Provider>

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

Context.Consumer

Метод React.createContext при вызове также возвращает консьюмеры — компоненты React, которые подписываются на изменения контекста от Provider.

const { Consumer } = NewContext;

Context.Consumer делает возможной подписку на контекст внутри функционального компонента.

<Consumer>
  {value => <span>{value}</span>}}
</Consumer>

Class.contextType

Свойство contextType позволяет компоненту использовать ближайшее значение объекта Context, которое было ему присвоено.

Пример:

class newComponent extends React.Component {
  render() {
    // use the context value assigned to the class.ContextType property
    {this.context}
  }
}
newComponent.contextType = NewContext;

Это свойство позволяет подписаться только на один контекст. В приведенном примере this.context упомянут внутри метода render(). Контекст также может быть упомянут в других методах жизненного цикла приложения, включая componentDidMount(), componentDidUpdate() и componentWillUnmount().

Context.displayName

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

Пример:

NewContext.displayName = 'NameOfContext'

При просмотре NewContext в React DevTools его имя должно выглядеть так NameOfContext:

<NewContext.Provider> // Отображается как NameOfContext.Provider
<NewContext.Consumer> // Отображается как NameOfContext.Consumer  

4. Пример использования Context API

Чтобы использовать React Context, вам нужно:

  1. Создать контекст с помощью вызова React.createContext(). Это вернет объект, который предоставляет компоненты Provider и Consumer.
  2. Объявить Provider, который получит ссылку на компонент, доступный в только что созданном объекте Context.
  3. Объявить Consumer, который также находится в объекте Context и используется для демонстрации значения пользователю.

Создаем объект Context

Начнем с того, что создадим новый проект на React:

npx create-react-app context-demo
cd context-demo
npm start

Теперь создадим в проекте файл theme.js, который будет содержать наш объект Context. Вот как должно выглядеть его содержимое:

// theme.js
import React from 'react';
const ThemeContext = React.createContext('light');
export default ThemeContext;

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

Объявляем Provider

Теперь создадим еще один файл — например, пусть он называется sample.js. Добавим в него компонент React, чтобы посмотреть, как работает контекст:

// sample.js
import React from 'react';
import Theme from './theme';
const Sample = () => (
<Theme.Provider value='dark'>
// Здесь должен быть Consumer
</Theme.Provider>
);
export default Sample;

В приведенном примере мы объявили стандартный компонент React, а также импортировали контекст из файла theme.js. Затем мы получили ссылку на провайдер, используя Theme.Provider. Но пока ничего не работает, потому что нет компонента Consumer, который принимает значение и показывает его пользователю.

Обратите внимание — Theme.Provider получил значение dark. При этом в файле theme.js мы задавали ему противоположное значение — light. Какой в этом был смысл, если мы все равно переопределили свойство в Provider? Ответ появится после объявления компонента Consumer.

Объявляем Consumer

Добавим Consumer в файл sample.js. Должно получиться так:

// sample.js

import React from 'react';
import Theme from './theme';
const Sample = () => (
<Theme.Provider value='dark'>
  <Theme.Consumer>  
  {theme => <div>Our theme is: {theme}</div>}   
  </Theme.Consumer>  
</Theme.Provider>
);
export default Sample;

Мы добавили компонент Theme.Consumer. Внутри него определяется значение темы. Затем его можно показать пользователям внутри <div>.

Теперь вернемся к вопросу, зачем сначала определять значение в контексте, а затем переопределять его в Provider. Ответ простой — чтобы у консьюмера всегда было какое-то значение, которое он использует.

Если не будет Provider, то Consumer применит значение по умолчанию из theme.js:

// theme.js
import React from 'react';
const ThemeContext = React.createContext('light');
export default ThemeContext;

Как только мы добавляем Provider с другим значением, Consumer начинает применять его:

const Sample = () => (
 <Theme.Provider value='dark'>
   <Theme.Consumer>
   {theme => <div>Theme value: {theme}</div>}
   </Theme.Consumer>
 </Theme.Provider>
)

Значение по умолчанию — это запасной вариант.

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

При использовании Context мы создаем и Provider, и Consumer. На практике их можно разделить. Например, перенести Consumer в другой файл. Пусть он будет называться ThemedButton.js:

// ThemedButton.js

import Theme from 'theme.js';
const ThemedButton = (props) => (
  <Theme.Consumer>
  {theme => <button { ...props }>button with them: {theme}</button>}
</Theme.Consumer>
);
export default ThemedButton

В таком случае содержимое файла sample.js тоже можно изменить:

// Sample.js

import React from 'react';
import Theme from './theme';
import ThemedButton from './ThemedButton';
const Sample = () => (
<Theme.Provider value='dark'>
  <ThemedButton />
</Theme.Provider>
);
export default Sample;

В этом примере значение из Provider передается через props. Благодаря этому мы можем внутри компонента ThemedButton получить доступ к свойству Theme через Consumer.

Другие примеры использования контекста, в том числе динамический контекст, смотрите в статье про React Context Hook.

5. Рекомендации по работе с Context

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

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

class newComponent extends React.Component {
  render() {
    return (
      <NewContext.Provider value={{color: 'blue'}}>
        <ProfilePage />
      </NewContext.Provider>
    )
  }
}

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

class newComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      value: { color: 'blue' }
    }
  }
  render() {
    return (
      <NewContext.Provider value={{this.state.value}}>
        <ProfilePage />
      </NewContext.Provider>
    )
  }
}

6. Заменяет ли контекст React Redux

И да, и нет. Ответ зависит от того, какие задачи вы решаете.

React Context подходит для простых ситуаций, когда нужно предоставить данные компонентам, но при этом не прокидывать пропсы по всему дереву. Он также позволяет управлять состоянием приложения с помощью useState/useReducer. Если состояние не нужно обновлять, то Context может стать оптимальным выбором.

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

Еще один сценарий использования Redux — для отслеживания и фиксации всех обновлений состояния.

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

Как и любой инструмент, React Context нужно использовать аккуратно.

  • Не используйте Context, чтобы избежать передачи свойств на один или два слоя. В этом нет смысла. Контекст подходит для управления состоянием в относительно больших приложениях. При передаче состояния на пару слоев использование пропсов выполняется быстрее.
  • Не используйте Context для хранения состояний, которые должны храниться локально. Например, в пользовательских формах.
  • Всегда оборачивайте в Provider самого нижнего общего родителя в дереве, а не компонент высокого уровня.
  • Мониторинг производительности и рефакторинг — необходимость. Особенно если при использовании контекста вы видите снижение скорости работы приложения.

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

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

Заключение

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

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

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

Обучение 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