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, вам нужно:
- Создать контекст с помощью вызова
React.createContext()
. Это вернет объект, который предоставляет компонентыProvider
иConsumer
. - Объявить
Provider
, который получит ссылку на компонент, доступный в только что созданном объектеContext
. - Объявить
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, посмотрите это тематическое видео:
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: