Кожен React-розробник рано чи пізно стикається з однією і тією ж перешкодою. Ви отримуєте об'єкт користувача всередині вашого головного компонента App. Потім передаєте його вниз. І ще нижче. Через обгортку роутера, через каркас лейауту, через контейнер сайдбару — і все це лише для того, щоб крихітний компонент аватара на три рівні глибше міг відобразити фото профілю. Компоненти посередині не переймаються цим об'єктом користувача. Вони просто пересилають посилку далі. Це і є prop drilling, і він перетворює чисте дерево компонентів на виснажливу гру в «глухий телефон».
Справжній біль починається тоді, коли змінюється структура цих даних. Наприклад, бекенд починає повертати user.profile.avatar замість user.avatar. Раптом вам доводиться редагувати TypeScript interfaces або PropTypes у п'яти файлах, які самі по собі ніколи не використовують ці дані. Саме тут на допомогу приходить React Context API.
Як Context змінює потік даних
Уявіть, що Context — це Wi-Fi роутер, який стоїть у центрі вашого будинку. Без нього вам довелося б прокладати Ethernet-кабелі через кожну кімнату, щоб подати сигнал на ноутбук. З ним роутер транслює сигнал через повітря, і будь-який пристрій із правильним паролем може підключитися напряму. Стіни не мають значення.
У термінах React, корінь вашого додатка може транслювати дані через дерево компонентів, не просячи кожен рівень виступати в ролі кур'єра. Будь-який вкладений компонент може підписатися на цю трансляцію і отримати саме те, що йому потрібно.
Три основні складові
Context API складається з трьох основних частин.
React.createContext() створює канал трансляції. Він повертає об'єкт, що містить Provider та (у застарілому коді) Consumer. Вам потрібно викликати це лише один раз для певної функції.
The Provider — це компонент, який огортає частину вашого дерева. Він приймає один пропс під назвою value. Усе, що ви передасте в цей пропс, стане доступним кожному нащадку, незалежно від глибини його розташування.
useContext — це Hook, який дозволяє функціональному компоненту підключитися до цієї трансляції. Всередині свого компонента ви передаєте створений об'єкт контексту в useContext, і він повертає поточне значення. Ось і все. Жодних обгорток, жодних зайвих пропсів.
До появи Hooks вам доводилося використовувати патерн Consumer із render props. Це працювало, але створювало багато зайвих відступів і нагромадження обгорток. useContext спростив усе це до одного рядка всередині тіла вашої функції.
Коли Context справді доречний
Не використовуйте Context просто за звичкою. Він створений для даних, які багато непов'язаних компонентів використовують у різних гілках вашого дерева. Гарними кандидатами є:
- Налаштування теми. Не лише світлий чи темний режим, а й токени відступів, палітри кольорів та масштаби шрифтів. Ручне прокидання цих значень через кожну стилізовану кнопку чи модальне вікно швидко набридає.
- Автентифікація користувача. Статус входу, масив дозволів або поточний об'єкт користувача. Ваш хедер, віджет панелі керування та захищений маршрут (private route guard) можуть знаходитися в різних кутках дерева.
- Мовні налаштування. Рядки локалі, формати дат та символи валют. Компоненти на глибинних рівнях, як-от мітки форм, потребують цього, причому кожен батьківський компонент на шляху не має про це знати.
- Дані кошика покупок. Кількість товарів, загальна вартість та функції додавання в кошик. Значок у хедері та сторінка оформлення замовлення потребують одного й того самого стану, але зазвичай вони знаходяться в абсолютно різних гілках макета.
Практичний перемикач тем
Один із найпростіших способів побачити Context у дії — це перемикач теми. Ось як це можна реалізувати, не пропускаючи важливі деталі.
По-перше, створіть файл ThemeContext.js. Викличте React.createContext() і збережіть результат. Потім створіть компонент ThemeProvider, який керує поточною темою за допомогою useState або useReducer. Огорніть children у Provider вашого контексту, передавши об'єкт, що містить як поточну тему, так і функцію для її перемикання. Експортуйте і ThemeProvider, і сам об'єкт контексту.
По-друге, перейдіть до точки входу вашого додатка. Імпортуйте ThemeProvider і огорніть ним увесь свій додаток. Якщо ви пропустите цей крок, усе, що намагатиметься прочитати контекст пізніше, побачить лише значення за замовчуванням.
По-третє, всередині компонента Header або Content імпортуйте об'єкт контексту та useContext. Викличте Hook, деструктуризуйте тему та функцію перемикання, і застосовуйте свої CSS-класи умовно. Додайте кнопку, яка викликає перемикання. Компонент ніколи не отримує пропс theme від свого батька. Він ловить сигнал прямо з повітря.
Prop Drilling, Context чи Redux?
Вибір між цими інструментами — це не стільки питання лояльності, скільки питання структури вашого стану.
Prop drilling цілком прийнятний для глибини у два або три рівні. Він є явним, його легко відстежити у вашій IDE, і він робить залежності очевидними. Проблеми виникають лише тоді, коли ви починаєте прокидати один і той самий проп крізь шість або сім рівнів.
Context API постачається разом із самим React. Це означає відсутність додаткового розміру бандла та необхідності зовнішнього налаштування. Він чудово справляється з глобальним станом малого та середнього розміру, особливо з даними, які змінюються рідко, як-от теми або профілі користувачів.
Redux потребує встановлення додаткових бібліотек і написання шаблонного коду (boilerplate). Він виправдовує себе, коли логіка стану є складною, коли кілька фрагментів (slices) стану глибоко взаємодіють між собою або коли вам потрібне налагодження з можливістю «подорожей у часі» (time-travel debugging) та middleware. Для простих глобальних даних Redux є надмірним.
Реалії продуктивності, про які ніхто не говорить
Ось у чому підвох, який відрізняє рішення джуніорів від рішень сеньйорів. Коли значення Context Provider змінюється, кожен компонент, що споживає цей контекст, перерендериться. Неважливо, чи залишився незмінним той конкретний фрагмент даних, який цікавить цей компонент. React бачить нове посилання та планує оновлення.
Якщо ви скинете весь стан вашого додатка в один гігантський StoreContext, ви фактично склеїте весь свій UI. Зміна налаштування теми призведе до перерендерингу кошика покупок, графіків на панелі керування та списку сповіщень. Це зайва робота.
Розділяйте контексти за доменами. Використовуйте ThemeContext для візуальних налаштувань, UserContext для даних профілю та CartContext для стану комерції. Якщо користувач змінить своє відображуване ім'я, ваш хедер оновиться, не чіпаючи сітку товарів. Також будьте обережні з тим, що ви передаєте у проп value провайдера. Якщо ви передаєте літерал об'єкта { theme, toggleTheme } безпосередньо під час рендерингу, ви створюєте нове посилання при кожному рендерингу, що спричиняє непотрібні оновлення. Стабілізуйте цю структуру за допомогою useMemo, якщо значення містить функції або не примітивні дані.
Помилки, що змарнують години
Дві помилки знову і знову стають пасткою для команд.
Забути експортувати об'єкт контексту. Легко експортувати компонент ThemeProvider, а потім спробувати викликати useContext(ThemeProvider). Це працює не так. Хуку потрібен об'єкт контексту, повернутий createContext, а не компонент-обгортка. Якщо ви експортуєте лише Provider, вашим споживачам (consumers) нічого буде імпортувати.
Виклик useContext поза межами його Provider. Хук повертає значення за замовчуванням, яке ви передали в createContext. Якщо ви не передали значення за замовчуванням, ви отримаєте undefined. Якщо ваше дерево компонентів рендерить споживача вище в DOM, ніж Provider, або якщо Provider взагалі відсутній, ваші дані просто не надійдуть. Перевірте, чи ваш файл index або root дійсно огортає додаток.
Головний висновок
React Context — це не революція в управлінні станом. Це цілеспрямований інструмент для вирішення конкретної просторової проблеми: передачі даних віддаленим компонентам, не перетворюючи кожен рівень на поштову службу. Використовуйте його для справді глобальних даних, розділяйте контексти за доменами для захисту продуктивності рендерингу та завжди огортайте своє дерево правильним Provider перед тим, як намагатися прочитати сигнал. Опануйте ці звички, і ваші дерева компонентів залишатимуться чистими, швидкими та зрозумілими.
