javascript

React Context: создание глобального стора, используя useContext и useState

  • суббота, 1 июля 2023 г. в 00:00:23
https://habr.com/ru/articles/745162/

Что такое 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пасибо