Что такое react-контекст?

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

Интерфейс react-контекста состоит из метода createContext, компонента Context.Provider и хука useContext. 

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

Для чего использовать контекст?

Цель создания контекста - это хранение и использование переменных, которые используются разными компонентами. 

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

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

Для того, чтобы реализовать приватность доступных в сторе данных, объект-хранилище можно создавать с помощью функции-генератора, а чтобы компоненты использующие стор ререндерились при изменении переменных стора, в хранилище их нужно объявлять с помощью хука useState.

Давайте на примере создания стора, посмотрим как использовать react-контекст.

Создание провайдера

Для того чтобы создать хранилище - в корне проекта создадим папку contexts. 

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

В нашем примере иерархия папок хранилища будет такая:

/contexts

  /AppContext

    AppContextProvider.jsx

    AppContext.js

Провайдер мы не меняем и после объявления будем только импортировать в jsx. А работать мы будем с контекстным хранилищем - добавлять в него общие методы и переменные.

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

Итак, объявим Context:

const Context = React.createContext(null);

Затем создадим провайдер от нашего контекста. Задача провайдера - обернуть компоненты, которые будут использовать глобальные переменные стора. Props провайдера будут содержать исходные величины, которые будут доступны при создании стора. 

Давайте объявим провайдер и назовем его AppContextProvider:

export const AppContextProvider = ({ children, ...props }) => {
  const context = new UseCreateAppContext(props);
  return <Context.Provider value={context}>{children}</Context.Provider>;
};

Здесь функция UseCreateAppContext создает объект-хранилище.

Вообще есть два способа организовать функцию, создающую объект-хранилище. Это либо использовать функцию-конструктор или функцию-генератор. Преимуществом использования функции-конструктора для создании стора является возможность сразу записывать в this-контекст поля, которые мы хотим использовать глобально, в месте их объявления. В то время как, если использовать функцию-генератор - т.е ту, которая явно возвращает объект - возвращаемые поля приходится отдельно дописывать в return в конце функции, а при больших сторах это становиться не удобно. Поэтому UseCreateAppContext будем реализовывать как функцию-конструктор.

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

Обратите внимание, что объект, генерируемый UseCreateAppContext, мы будем использовать в компонентах не на прямую, но получать его через useContext. Так величины контекстного хранилища будут находиться в памяти при ререндерах.

Так для получения контекстного хранилища, нужно использовать хук useContext с этим контекстом.

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

export function useAppContext() {
  const context = React.useContext(Context);
  if (!context) throw new Error('Use app context within provider!');
  return context;
}

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

Последним для создания стора осталось объявить саму функцию UseCreateAppContext, возвращающую объект который мы будем хранить в Provider value.

Исходные значения props - это те значения, которые получит компонент AppContextProvider.

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

Дальше содержимое стора можно наполнить чем угодно. 

Давайте объявим такой стор:

export const UseCreateAppContext = function(props) {
  const [test, setTest] = useState(props.test || 'Hello world');
  this.test = test;
 
  this.toggleTest = useCallback(() => {
    setTest(_test => (_test === 'Hi' ? 'You are awesome' : 'Hi'));
  });
}

Теперь чтобы использовать контекстное хранилище в каком то компоненте, его нужно обернуть в провайдер:

<AppContextProvider>
  <MyComponent />
</AppContextProvider>

Мы можем задать исходное значение переменной test:

<AppContextProvider test={‘Hello’}>
  <MyComponent />
</AppContextProvider>

Затем, в компоненте MyComponent вызвать useAppContext:

const appContext = useAppContext();

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

const {test, toggleTest} = useAppContext();

И затем обращаться к нужным переменным:

console.log(test);
toggleTest();

При изменении переменной test приведет к ререндеру компонента, тк в сторе она храниться в хуке useState.

Собственно, вот и вся магия. 

Теперь содержимое UseCreateAppContext можно менять на свое усмотрение и обращаться к нему глобально.

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

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

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

Почитать подробнее про контекст можно в документации react.

Cпасибо

Комментарии (9)


  1. KhodeN
    01.07.2023 21:39
    +1

    this внутри функционального компонента, это уже за гранью добра и зла.

    Чем useRef не угодил?


    1. ko22012
      01.07.2023 21:39
      +1

      Скорее всего у автора путаница с классовыми компонентами


    1. karmacan Автор
      01.07.2023 21:39

      this находиться не в классовом компоненте в функции-конструкторе, которая просто создает обьект, который потом используется в функциональном компоненте провайдера для помещения в контекст )


      1. KhodeN
        01.07.2023 21:39

        UseCreateAppContext - ну не пишут так, что это?

        Если класс, то почему в виде функции, сейчас 2023 год.

        Если функциональный компонент - то весь его стейт должен хранится в хуках. Если нужно автоматическое обновление (реактивность), то в useState/useReducer. Если обновление вручную - то можно использовать "сырой" useRef. Есть же руководство Roles of hooks.

        А вообще, использовать useContext в качестве глобального стора для большого приложения - это дикие проблемы с производительностью (постоянный ререндер на каждый чих).

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

        Если нужен реально глобальный (обычно не нужен) стор, то redux toolkit + reselect (для кеширования, чтобы не ререндерить).

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


  1. Alexandroppolus
    01.07.2023 21:39

    new UseCreateAppContext(props);

    Впервые вижу оформление хука как функцию-конструктор, и вызов через new )


    1. karmacan Автор
      01.07.2023 21:39

      не хотелось писать в статью без оригинальных идей )

      если вы не хотите использовать функцию-конструктор - UseCreateAppContext можно спокойно сделать функцией с явным return - этот вариант более популярный, но на мой взгляд удобнее возвращаемые переменные записывать в this


      1. Alexandroppolus
        01.07.2023 21:39

        У этого подхода есть минусы. Например, react-hooks/rules-of-hooks не сможет проверить, что хук вызван внутри условия или цикла


  1. fancy-apps
    01.07.2023 21:39

    мда...


  1. Kushin
    01.07.2023 21:39

    Можно было просто ссылку на документацию оставить. Желательно не на легаси https://react.dev/