Одна из важнейших и наиболее сложных задач в разработке современных веб-приложений — управление их состоянием. Для этого есть уйма библиотек (Redux, например), часть из них создана поверх уже готовых решений. Однако есть интересный способ обойтись и вовсе без подключения внешних библиотек (React Hooks + context). Об этом мы сегодня и поговорим.
Содержание:
1. Что такое React Context?
2. Когда использовать контекст?
3. Перед тем, как вы начнете использовать контекст
4. API
4.1. React.createContext
4.2. Provider
4.3. Consumer
5. Примеры
5.1. Динамический контекст
5.2. Обновление контекста из вложенного компонента
5.3. Потребление множества контекстов
5.4. Доступ к контексту в методах жизненного цикла
5.5. Получение контекста старшим компонентом (HOC’ом)
5.6. Передача ссылок ref потребителям контекста
5.7. Возможные проблемы
5.8. Использование устаревшей версии API
6. Вывод
В общем случае React Context — это один из способов передачи данных между компонентами приложения. Если говорить точнее, это передача глобальных props (пропсов), которые доступны в рамках всего приложения. А сами пропсы — это данные на входе React-компонентов, которые передаются от родительского компонента к дочернему в пределах древовидной структуры. Иначе говоря, контекст предназначен для передачи данных от одного вложенного компонента к другому.
Алгоритм можно визуализировать примерно так:
Необходимое нам состояние расположено в компоненте App
. Оно необходимо для компонентов UserProfile
и UserDetails
, для чего мы будет передавать его вниз по дереву, вот так:
А вот так выглядит код.
Рассмотрим пример:
const ColorContext = React.createContext("yellow") class App extends React.Component { render() { return ( <ColorContext.Provider> <P /> </ColorContext.Provider> ) } } class P extends React.Component { render() { return ( {this.context} <C /> ) } } class C extends React.Component { render() { return ( <Sub-C /> ) } } class Sub-C extends React.Component { render() { <div> {this.context} </div> } }
В коде есть три компонента: App -> P -> C -> Sub-C
. Здесь App
отображает P
, P
отображает C
, а C
отображает Sub-C
, именно последний определяется как context с именем ColorContext
. В состав ColorContext.Provider
входит P
, что позволяет ему получать доступ к данным, которые определены в ColorContext
.
Таким образом, все дочерние компоненты P
могут получать доступ к данным, которые определены ранее. Для этого выполняется {this.context}
.
Чтобы React знал, что нужно передать context
от родительского компонента к дочерним элементам, необходимо сначала определить два атрибута в родительском классе. Это следующие атрибуты:
childContextTypes
,getChildContext
.Теперь, чтобы извлечь context внутри дочернего компонента, определяем в нем contextTypes
. При этом отметим, что дочерние компоненты тоже могут менять значение context
, что будет отображаться на всех вложенных компонентах, в том числе и на родительском.
Как и любой инструмент, контекст хорош в том случае, когда другие альтернативы будут более сложными. Также использование контекста позволяет избежать передачи свойств в промежуточные компоненты. Ведь если в древовидной структуре есть десять и больше уровней вложенности, это может быть сложно реализовать.
Так выглядит пример, где передается пропс theme
:
class App extends React.Component { render() { return <Toolbar theme="dark" />; } } function Toolbar(props) { return ( <div> <ThemedButton theme={props.theme} /> </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; } }
Использование контекста позволит не передавать пропсы в промежуточные компоненты, а вызывать в тех, где этот компонент необходим.
//Создадим контекст для текущей // UI-темы (со значением "light" по умолчанию). const ThemeContext = React.createContext('light'); class App extends React.Component { render() { // Компонент Provider используется для передачи текущей // UI-темы вниз по дереву. Любой компонент может использовать // этот контекст и не важно, как глубоко он находится. // В этом примере мы передаем "dark" в качестве значения контекста. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // Компонент, который находится в середине, // больше не должен явно передавать тему вниз. function Toolbar() { return ( <div> <ThemedButton /> </div> ); } class ThemedButton extends React.Component { // Определяем contextType, чтобы получить значение контекста. // React найдет (выше по дереву) ближайший Provider-компонент, // предоставляющий этот контекст, и использует его значение. // В этом примере значение UI-темы будет "dark". static contextType = ThemeContext; render() { return <Button theme={this.context} />; } }
Таким образом можно избежать избыточности при передаче данных и упростить работу кода в целом.
Как и в любом другом случае, контекст хорошо применять только там, где это нужно. Сферой его применения становится ситуация, когда необходимо обеспечить доступ данных во многих компонентах на разных уровнях вложенности. При этом важно, что данные используются один раз, но во многих местах. Если контекст приходится использовать повторно, то лучше обойтись без него.
Одним из альтернативных способов является передача лишь части пропсов, это реализовано с помощью композиции компонентов. К примеру, вот так выглядит компонент Page
, который передает пропсы user
и avatarSize
на несколько уровней вниз. Это нужно, чтобы пропсы user
и avatarSize
можно было использовать в компонентах Link
и Avatar
.
Код этой идеи выглядит так:
<Page user={user} avatarSize={avatarSize} /> // ... который рендерит ... <PageLayout user={user} avatarSize={avatarSize} /> // ... который рендерит ... <NavigationBar user={user} avatarSize={avatarSize} /> // ... который рендерит ... <Link href={user.permalink}> <Avatar user={user} size={avatarSize} /> </Link>
Передача пропсов user
и avatarSize
избыточная, учитывая, что только компонент Avatar
использует их. Если потребуется больше пропсов, то их придется добавить на все промежуточные уровни. Для решения можно передать вниз только компонент Avatar
. Тогда промежуточные компоненты не знают о пропсах user
и avatarSize
.
Код такой:
function Page(props) { const user = props.user; const userLink = ( <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> ); return <PageLayout userLink={userLink} />; } // Теперь, это выглядит следующим образом: <Page user={user} avatarSize={avatarSize}/> // ... который рендерит ... <PageLayout userLink={...} /> // ... который рендерит ... <NavigationBar userLink={...} /> // ... который рендерит ... {props.userLink}
Поступая таким образом, можно сильно упростить себе жизнь.
В этом разделе мы более предметно и с примерами поговорим о различных элемента контекста, что они делают и зачем нужны.
Создание контекста — самая простая операция. Код выглядит так:
const {Provider, Consumer} = React.createContext(defaultValue);
Здесь создается пара поставщик (Provider
) и потребитель (Consumer
).
Логика работы выглядит так:
Consumer
) будет считываться текущее значение контекста.Provider
), который находится выше в иерархическом дереве.Отметим, что аргумент defaultValue
используется в тех случаях, когда в дереве нет поставщика. Это позволяет тестировать компонент изолированно.
Код поставщика выглядит так:
<Provider value={/* some value */}>
Этот компонент дает возможность потребителям подписываться на изменения контекста и получать их. Он принимает свойство value
, которое затем передается дальше потребителям.
В свою очередь потребители выступают потомками текущего поставщика, у них есть связь «один ко многим», то есть один поставщик может быть родителем многих потребителей. Допускается также вложенность поставщиков, что позволяет передавать данные глубже в дерево.
Код потребителя выглядит так:
<Consumer> {value => /* отрисовывает что-то, что основано на значении контекста */} </Consumer>
Этот компонент React подписывается на изменения контекста и получает данные от поставщика. Функция используется в качестве дочернего элемента, она принимает текущее значение контекста и на выходе возвращает узел React.
Также потребитель имеет аргумент value
, передаваемый функции. Он равен свойству value
ближайшего поставщика в рамках контекста выше по дереву. Если поставщика нет, то аргумент примет значение defaultValue
, которое было определено в рамках createContext().
Теперь рассмотрим некоторые примеры использования контекста в коде.
Это более сложный пример с динамическими значениями для темы оформления:
Код для theme-context.js
(код самого контекста):
export const themes = { light: { foreground: '#ffffff', background: '#222222', }, dark: { foreground: '#000000', background: '#eeeeee', }, }; export const ThemeContext = React.createContext( themes.dark // значение по умолчанию );
Код для themed-button.js
(Код кнопки):
import {ThemeContext} from './theme-context'; function ThemedButton(props) { return ( <ThemeContext.Consumer> {theme => ( <button {...props} style={{backgroundColor: theme.background}} /> )} </ThemeContext.Consumer> ); } export default ThemedButton;
Код для app.js
import {ThemeContext, themes} from './theme-context'; import ThemedButton from './themed-button'; // Промежуточный компонент, который использует ThemedButton function Toolbar(props) { return ( <ThemedButton onClick={props.changeTheme}> Change Theme </ThemedButton> ); } class App extends React.Component { constructor(props) { super(props); this.state = { theme: themes.light, }; this.toggleTheme = () => { this.setState(state => ({ const { light, dark } = themes theme: state.theme === dark ? light : dark })); }; } render() { // Кнопка ThemedButton внутри ThemeProvider // использует тему из состояния, в то время как снаружи // использует тему по умолчанию: dark return ( <Page> <ThemeContext.Provider value={this.state.theme}> <Toolbar changeTheme={this.toggleTheme} /> </ThemeContext.Provider> <Section> <ThemedButton /> </Section> </Page> ); } } ReactDOM.render(<App />, document.root);
В ряде случаев может быть нужно обновлять контекст из компонента, который находится на большом уровне вложенности в дереве компонентов. Для этого можно передавать функцию через контекст, что позволит потребителям обновлять данные.
Код theme-context.js
:
// Убедитесь, что форма значения по умолчанию, переданная в // createContext, соответствует форме, которую ожидают потребители! export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, }); Код theme-toggler-button.js import {ThemeContext} from './theme-context'; function ThemeTogglerButton() { // Компонент ThemeTogglerButton принимает не только тему, // но и функцию toggleTheme из контекста return ( <ThemeContext.Consumer> {({theme, toggleTheme}) => ( <button onClick={toggleTheme} style={{backgroundColor: theme.background}}> Toggle Theme </button> )} </ThemeContext.Consumer> ); } export default ThemeTogglerButton;
Код app.js
import {ThemeContext, themes} from './theme-context'; import ThemeTogglerButton from './theme-toggler-button'; class App extends React.Component { constructor(props) { super(props); this.toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.dark ? themes.light : themes.dark, })); }; // Состояние также содержит обновляющую функцию, поэтому она // будет передана в поставщик контекста this.state = { theme: themes.light, toggleTheme: this.toggleTheme }; } render() { // Состояние целиком передается в поставщик return ( <ThemeContext.Provider value={this.state}> <Content /> </ThemeContext.Provider> ); } } function Content() { return ( <div> <ThemeTogglerButton /> </div> ); } ReactDOM.render(<App />, document.root);
Зачастую в коде используется не один, а несколько контекстов. Это позволяет быстро перерисовывать каждого потребителя контекста в отдельном узле дерева.
Код выглядит так:
// Контекст темы. Светлая тема по умолчанию. const ThemeContext = React.createContext('light'); // Контекст Signed-in пользователя const UserContext = React.createContext(); class App extends React.Component { render() { const {signedInUser, theme} = this.props; // Компонент приложения, который предоставляет начальные значения контекста return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Layout /> </UserContext.Provider> </ThemeContext.Provider> ); } } function Layout() { return ( <div> <Sidebar /> <Content /> </div> ); } // Компонент может потреблять множество контекстов function Content() { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); }
Если в процессе используются два или большее число значений контекста, можно создать свой собственный компонент render prop
, который и будет представлять их.
Это еще одна часто встречающаяся ситуация. Вместо того, чтобы добавлять контекст в каждый метод жизненного цикла, его можно передавать в качестве свойства, после чего работать с ним уже как с обычным свойством.
Пример кода:
class Button extends React.Component { componentDidMount() { // this.props.theme - текущее значение контекста } componentDidUpdate(prevProps, prevState) { // prevProps.theme - предыдущее значение контекста // this.props.theme - новое значение контекста } render() { const {theme, children} = this.props; return ( <button className={theme ? 'dark' : 'light'}> {children} </button> ); } } export default props => ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> );
Учитывая, что некоторые виды контекста могу потреблять разные компоненты, вместо элемента <Context.Consumer>
, который позволяет обертывать каждую зависимость, можно использовать старший компонент. Это часто применяется для темы оформления или локализации.
Код выглядит так для нескольких компонентов:
const ThemeContext = React.createContext('light'); function ThemedButton(props) { return ( <ThemeContext.Consumer> {theme => <button className={theme} {...props} />} </ThemeContext.Consumer> ); }
А вот так выглядит контекст с помощью старшего компонента с withTheme
:
const ThemeContext = React.createContext('light'); // Эта функция принимает компонент... export function withTheme(Component) { // ... возвращает другой компонент... return function ThemedComponent(props) { // ... и отрисовывает обернутый компонент с темой из контекста! // Обратите внимание, что мы с таким же успехом можем передавать // любое дополнительное свойство return ( <ThemeContext.Consumer> {theme => <Component {...props} theme={theme} />} </ThemeContext.Consumer> ); }; }
Таким образом, любой компонент, который зависит от контекста темы, сможет использовать пользовательскую функцию withTheme
:
Код выглядит так:
function Button({theme, ...rest}) { return <button className={theme} {...rest} />; } const ThemedButton = withTheme(Button);
Проблемой render prop
в рамках API-интерфейса является то, что ссылки ref
не передаются автоматически обернутым элементам. Для этого надо использовать React.forwardRef
.
Код fancy-button.js
class FancyButton extends React.Component { focus() { // ... } // ... } // Используйте контекст, чтобы передать текущую "theme" в FancyButton. // Используйте forwardRef, чтобы с тем же успехом передавать ссылки ref в FancyButton. export default React.forwardRef((props, ref) => ( <ThemeContext.Consumer> {theme => ( <FancyButton {...props} theme={theme} ref={ref} /> )} </ThemeContext.Consumer> ));
Код app.js
import FancyButton from './fancy-button'; const ref = React.createRef(); // Наша ссылка ref будет указывать на компонент FancyButton, // а не на ThemeContext.Consumer, который его оборачивает. // Это означает, что мы можем вызывать FancyButton методы как ref.current.focus() <FancyButton ref={ref} onClick={handleClick}> Click me! </FancyButton>;
Контекст использует ссылочную идентификацию, которая позволяет определить, когда нужно перерисовывать данные. Из-за этого могут возникнуть ситуации, когда система будет перерисовывать потребителей каждый раз, когда перерисовывается поставщик. Это происходит из-за того, что для value
всегда создается новый объект:
Пример такого проблемного кода:
class App extends React.Component { render() { return ( <Provider value={{something: 'что-нибудь'}}> <Toolbar /> </Provider> ); } }
Для обхода проблемы значение value
переводим в состояние родителя.
Пример кода:
class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'что-нибудь'} }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
Таким образом можно решить проблему с перерисовкой.
Отметим, что ранее в React использовалась экспериментальная версия API контекста. Она актуальна для ветки 16.x, но все приложения, использующие API, должны перейти на новую версию.
На начало 2022 года актуальной версией является 17.0.2.
Как видим, использование контекста позволяет решить некоторые проблемы и задачи куда легче, чем варианты, предлагавшиеся ранее. С другой стороны, это инструмент для строго определенных задач, так что его надо уметь использовать с умом. Однако в некоторых случаях он реально позволяет существенно улучшить ситуацию.
Помимо этого материала, наглядно посмотреть, что собой представляет контекст в React можно по видеоссылке ниже:
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…