Сегодня на уроке 20-м мы будем говорить о проблеме, с которой сталкивается каждый React-разработчик и об изящном способе её решения с помощью хука useContext.
Если вы дошли до этого урока, вы уже отлично владеете основами: компоненты, состояние с useState, побочные эффекты с useEffect. Вы видели, как данные передаются сверху вниз по дереву компонентов через пропсы. Это фундаментальный принцип React, его «однонаправленный поток данных». Но что случается, когда вам нужно передать данные очень глубоко, через множество уровней компонентов? Правильно, начинается «пропс дриллинг». Давайте с него и начнем.
Почему нужно избавляться от «Пропс Дриллинг»?
Представьте себе такую ситуацию. У вас есть корневой компонент App, в котором хранится, например, объект с данными текущего пользователя: его имя, аватар, тема оформления. Внутри App у вас есть компонент Header, внутри Header компонент UserMenu, а внутри UserMenu маленькая кнопка Avatar, которая как раз и должна отображать картинку профиля.
Чтобы передать user.avatarUrl из App в Avatar, вам придется «пробросить» этот пропс через Header и UserMenu. Ни Header, ни UserMenu сами по себе этой информации не нужны. Они выступают в роли простых «курьеров», передающих данные дальше. Это и есть «пропс дриллинг» (prop drilling), процесс проталкивания пропсов через множество уровней компонентов, которые сами эти пропсы не используют.
Сначала это кажется не такой уж большой проблемой. Но с ростом приложения появляются новые сложности:
-
Сложность рефакторинга. Если вам нужно изменить структуру данных или добавить новый пропс, придется править все промежуточные компоненты.
-
«Загрязнение» кода. Компоненты, которые не используют пропсы, вынуждены их объявлять и передавать, что делает их менее чистыми и усложняет понимание их ответственности.
-
Низкая производительность разработки. Постоянно следить за цепочкой передачи пропсов утомительно и чревато ошибками.
Именно для решения этой проблемы в React была введена концепция Context.
Знакомимся с React Context
React Context это механизм, который позволяет передавать данные через дерево компонентов без необходимости передавать пропсы на каждом уровне. Грубо говоря, он создает своего рода «глобальное» пространство данных для определенной части вашего приложения (или для всего приложения целиком).
Представьте себе Context как трубопровод, который вы проводите прямо от источника данных (например, от корневого компонента App) к любому компоненту, который находится «ниже по склону», минуя все промежуточные уровни. Компоненты, которым нужны данные из этого контекста, могут просто «подключиться» к этой трубе и забрать оттуда всё необходимое.
Context не заменяет собой стейт-менеджеры вроде Redux или MobX для очень сложных приложений с высокочастотными обновлениями данных. Но для 90% задач, таких как тема оформления, данные пользователя, выбранный язык (i18n), он является идеальным и простым решением.
Создаем свой первый Context: React.createContext()
Для этого используется функция React.createContext(). Её результатом является объект контекста, который содержит два важнейших компонента: Provider и Consumer. Но с появлением хуков useContext, Consumer используется реже и мы сфокусируемся на современном подходе.
Давайте создадим контекст для темы нашего приложения.
// ThemeContext.js import { createContext } from 'react'; // Создаем сам контекст. // Значение, переданное в createContext, является "значением по умолчанию". // Оно будет использовано, если компонент окажется вне провайдера. // Это полезно для изолированного тестирования компонентов. export const ThemeContext = createContext('light'); // 'light' - значение по умолчанию
Вот и все! Мы создали объект ThemeContext. Пока что это просто «коробка» для наших данных. Сами данные и механизм их распространения обеспечиваются другим компонентом, Provider.
Обеспечиваем данные: Компонент Provider
Provider это компонент, который поставляется вместе с созданным контекстом. Его задача «обеспечить» дочерние компоненты данными. Вы оборачиваете часть дерева компонентов в этот провайдер и передаете ему актуальные данные через проп value.
Лучше всего размещать провайдера на как можно более высоком уровне в том месте дерева, где всем нижележащим компонентам могут понадобиться эти данные. Часто это корневой компонент App или какой-то крупный раздел приложения.
Давайте обеспечим наше приложение темой.
// App.js import React, { useState } from 'react'; import { ThemeContext } from './ThemeContext'; import Header from './Header'; import MainContent from './MainContent'; function App() { // Состояние темы теперь управляется здесь, на верхнем уровне. const [theme, setTheme] = useState('light'); // Функция для переключения темы const toggleTheme = () => { setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light'); }; return ( // Оборачиваем все наше приложение в ThemeContext.Provider. // В value передаем текущую тему и функцию для её смены. // Теперь любой компонент внутри этого провайдера сможет получить доступ к theme и toggleTheme. <ThemeContext.Provider value={{ theme, toggleTheme }}> <div className={`app ${theme}`}> <Header /> <MainContent /> </div> </ThemeContext.Provider> ); } export default App;
Обратите внимание на несколько ключевых моментов:
-
Мы передаем в
valueне просто строку, а объект, содержащий и саму тему и функцию для её изменения. Это очень распространенный паттерн. -
Каждый раз, когда изменяется состояние
themeв компонентеApp, React автоматически оповестит всех «подписчиков» этого контекста (тех, кто используетuseContext(ThemeContext)) и вызовет их перерендер. -
Важно: Если объект в
valueменяется при каждом рендере (например,value={{theme, toggleTheme}}создает новый объект каждый раз), это может вызвать ненужные перерендеры даже у компонентов, которые от этого объекта не зависят. В реальных приложениях для оптимизации часто используютuseMemo.
Потребляем данные: Хук useContext
Мы научились создавать контекст и обеспечивать его данными. Как же эти данные получить в любом дочернем компоненте, например, в кнопке переключения темы, которая находится глубоко в недрах Header?
Для этого и существует хук useContext. Он принимает в качестве аргумента объект контекста (который мы создали с помощью createContext) и возвращает текущее значение этого контекста. То самое значение, которое мы передали в проп value ближайшего родительского Provider.
Давайте создадим компонент ThemeToggleButton.
// ThemeToggleButton.js import React, { useContext } from 'react'; import { ThemeContext } from './ThemeContext'; function ThemeToggleButton() { // Используем хук useContext, передав в него ThemeContext. // Теперь переменная themeContext будет содержать тот самый объект { theme, toggleTheme }, // который мы передали в провайдер в App.js. const themeContext = useContext(ThemeContext); // Если компонент используется вне провайдера ThemeContext.Provider, // themeContext будет равен значению по умолчанию, т.е. 'light'. // В нашем случае это объект, поэтому нужно быть осторожным. // Лучшая практика, это всегда использовать провайдер. return ( <button onClick={themeContext.toggleTheme}> Переключить на {themeContext.theme === 'light' ? 'тёмную' : 'светлую'} тему </button> ); } export default ThemeToggleButton;
И всё! Дело в том, что компоненту ThemeToggleButton больше не нужно получать theme и toggleTheme через пропсы. Он может «достать» их из контекста напрямую, где бы он ни находился. Мы полностью избавились от необходимости передавать эти пропсы через Header, UserMenu и другие компоненты.
Создаем контекст для данных пользователя
Давайте закрепим знания на более комплексном примере. Создадим контекст для данных авторизованного пользователя.
1. Создаем контекст:
// UserContext.js import { createContext } from 'react'; // Создаем контекст. В качестве значения по умолчанию передаем null, // что будет означать "пользователь не авторизован". export const UserContext = createContext(null);
2. Обеспечиваем контекст в App:
// App.js import React, { useState, useEffect } from 'react'; import { UserContext } from './UserContext'; import { ThemeContext } from './ThemeContext'; import Header from './Header'; import Dashboard from './Dashboard'; function App() { const [user, setUser] = useState(null); // Данные пользователя const [theme, setTheme] = useState('light'); // Имитируем загрузку пользователя (например из API) useEffect(() => { setTimeout(() => { setUser({ name: 'Максим', email: 'max@example.com', avatarUrl: '/path/to/avatar.jpg' }); }, 1000); }, []); const toggleTheme = () => { setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light'); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {/* Оборачиваем приложение в UserContext.Provider и передаем туда и user и setUser. Это позволит не только читать данные пользователя, но и обновлять их (например, при выходе из системы). */} <UserContext.Provider value={{ user, setUser }}> <div className={`app ${theme}`}> <Header /> <main> {user ? <Dashboard /> : <p>Загрузка...</p>} </main> </div> </UserContext.Provider> </ThemeContext.Provider> ); } export default App;
3. Потребляем в компонентах:
Теперь любой компонент может легко получить доступ к данным пользователя.
// Header.js import React, { useContext } from 'react'; import { UserContext } from './UserContext'; function Header() { // Получаем объект { user, setUser } из контекста const { user } = useContext(UserContext); return ( <header> <h1>Мое Приложение</h1> {user && <p>Добро пожаловать, {user.name}!</p>} </header> ); }
// UserProfile.js import React, { useContext } from 'react'; import { UserContext } from './UserContext'; function UserProfile() { const { user, setUser } = useContext(UserContext); const handleLogout = () => { // При выходе просто очищаем данные пользователя setUser(null); }; return ( <div className="user-profile"> <img src={user.avatarUrl} alt="Аватар" /> <h2>{user.name}</h2> <p>{user.email}</p> <button onClick={handleLogout}>Выйти</button> </div> ); }
Видите, насколько это чище и проще? Компонент Header может показать приветствие, а UserProfile вывести детальную информацию и обработать выход и ни один из них не получал данные через длинную цепочку пропсов.
Когда использовать Context?
Context не стоит применять для данных, которые используются только одним или двумя компонентами-соседями. В этом случае передача пропсов, более простое и явное решение.
Используйте Context для:
-
Данных, которые можно охарактеризовать как «глобальные»: тема, пользователь, выбранный язык.
-
Данных, которые требуются многим компонентам в разных частях приложения (например, кэш данных, состояние модального окна).
-
Избавления от пропс дриллинга, когда промежуточные компоненты не используют эти данные.
Не используйте Context для:
-
Часто и интенсивно меняющихся данных (например, положение курсора мыши, значение поля ввода в реальном времени). Это может привести к проблемам с производительностью, так как все подписчики будут перерендериваться при каждом изменении.
-
Локализованного состояния, которое касается только одного-двух компонентов.
Практические задачи для закрепления
Чтобы уверенно овладеть useContext, выполните следующие задачи:
Задача 1 (Базовый уровень)
-
Создайте новое React-приложение или откройте существующее.
-
Реализуйте контекст для темы, как в примерах выше.
-
Создайте компонент
Footer, который будет отображать текст «Текущая тема: [название темы]». ИспользуйтеuseContext, чтобы получить тему внутриFooter. -
Убедитесь, что при переключении темы кнопкой в
Header, текст вFooterтакже обновляется.
Задача 2 (Средний уровень)
-
Создайте контекст
NotificationContextдля уведомлений. -
В провайдере храните состояние
notification(может бытьnullили объект{text: string, type: 'success' | 'error'}) и функциюshowNotificationдля его обновления. -
Реализуйте компонент
Notification(всплывающее окно), который подписан на этот контекст и отображает уведомление, если оно есть. -
В компоненте
Dashboardдобавьте кнопки «Показать успех» и «Показать ошибку», которые при нажатии вызываютshowNotificationиз контекста.
Задача 3 (Продвинутый уровень): Комбинация useReducer + useContext
Этот паттерн часто называют «бедный Redux» и он невероятно мощный.
-
Создайте контекст
CartContextдля корзины покупок. -
Внутри провайдера используйте хук
useReducerдля управления состоянием корзины (массив товаров, общая сумма). -
Редуктор должен обрабатывать действия:
ADD_ITEM,REMOVE_ITEM,CLEAR_CART. -
В
valueпровайдера передайте и само состояние (state) иdispatchфункцию. -
Создайте компоненты
ProductList(добавляет товары в корзину) иCart(показывает товары и позволяет их удалять). Оба должны использоватьuseContext(CartContext)для диспатча действий и отображения состояния.
Пример редуктора для корзины:
// cartReducer.js export const cartReducer = (state, action) => { switch (action.type) { case 'ADD_ITEM': // Логика добавления товара... return newState; case 'REMOVE_ITEM': // Логика удаления товара... return newState; case 'CLEAR_CART': return { items: [], total: 0 }; default: return state; } };
Заключение
Вы научились создавать контексты, обеспечивать данные с помощью провайдера и потреблять их в любом компоненте с помощью хука. Теперь вы вооружены мощным инструментом для борьбы с пропс дриллингом и организации «глобального» состояния в ваших приложениях.
Помните, что с великой силой приходит великая ответственность. Не злоупотребляйте контекстом, применяйте его там, где это действительно оправдано.
Это урок из моего полного курса с уроками по React для начинающих. Там вы найдете все 30 уроков, начиная с основ JSX и заканчивая продвинутыми паттернами и развертыванием приложения. Переходите по ссылке и продолжайте обучение!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


