Что такое 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 = useCreateAppContext(props);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
Здесь функция 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');
const toggleTest = useCallback(() => {
setTest(_test => (_test === 'Hi' ? 'You are awesome' : 'Hi'));
});
return {
test,
toggleTest,
};
}
Теперь чтобы использовать контекстное хранилище в каком то компоненте, его нужно обернуть в провайдер:
<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пасибо

** Примечание:
Статья была изменена, принимая комментарии @KhodeN и @Alexandroppolus - для создания объекта, который храниться в контексте лучше использовать функцию-генератор, а не функцию-конструктор. Это дает более стандартизированную запись и защищает от возможных ошибок при вызове хуков из условия.