Кожен React-розробник рано чи пізно стикається з одним і тим самим питанням: чи варто використовувати Context, чи це проблема для Redux? Якщо ви займаєтеся розробкою лише кілька місяців, інформаційний шум в інтернеті створює враження, ніби це вибір між «або-або». Деякі туторіали ставляться до Redux як до застарілого тягаря. Інші попереджають, що Context не зможе масштабуватися далі списку справ (to-do list). Жоден із цих крайнощів не є корисним. Правда полягає в тому, що ці інструменти вирішують різні типи проблем, і розумний вибір залежить від того, що саме робить ваш застосунок.
Проблема Prop Drilling
Перш ніж обирати стратегію управління станом, варто зрозуміти «хворобу», яку намагаються вилікувати обидва інструменти. Уявіть, що ви створюєте сайт електронної комерції. Ви отримуєте профіль користувача на верхньому рівні компонента App. А десь у футері маленькому компоненту AccountLink потрібна ця фотографія профілю. Без глобального сховища об'єкт користувача має пройти через Home, потім Header, потім NavContainer, потім UserDropdown і, нарешті, потрапити в AccountLink. Кожен проміжний рівень торкається даних, які він не використовує. Це і є prop drilling.
Prop drilling робить компоненти крихкими. Рефакторинг стає ризикованим, тому що видалення одного «посередника» розриває весь ланцюг. Повторне використання страждає, оскільки компоненти вимагають пропси, які вони лише передають далі вниз. І Context, і Redux усувають це, дозволяючи віддаленим компонентам підписуватися на спільні дані напряму. Проте спосіб, у який вони надають ці дані, і ціна цього процесу швидко розходяться.
Коли React Context API підходить найкраще
React Context вбудований у саму бібліотеку. Жодних додаткових інсталяцій через npm, жодної конфігурації збірки, жодних шаблонних файлів (boilerplate). Ви створюєте об'єкт контексту, огортаєте частину свого дерева в Provider і споживаєте значення за допомогою useContext у будь-якому вкладеному компоненті. Завдяки такій простоті Context чудово проявляє себе в проєктах малого та середнього розміру, де зміни стану відбуваються рідко, а структура цього стану є відносно пласкою.
Подумайте про теми інтерфейсу. Користувач перемикається між світлою та темною темами, можливо, лише раз за сесію. Значення поширюється на кожен styled component, але воно змінюється настільки рідко, що питання продуктивності майже не виникають. Статус автентифікації — ще один класичний приклад. Після того, як користувач увійшов у систему, прапорець isAuthenticated та об'єкт user залишаються стабільними під час десятків переходів між сторінками. Налаштування мови або локалізації працюють так само. Це широкі, повільні сигнали, які потрібні багатьом компонентам, але які змінюють лише одиниці.
Підвох полягає в тому, як Context обробляє оновлення. Коли значення Context Provider змінюється, React перерендерить кожен окремий компонент, який споживає цей контекст. У невеликому застосунку ви цього не помітите. У більшому застосунку, якщо ви розмістите дані, що швидко змінюються, всередині широко використовуваного Context, ви спровокуєте каскад марних рендерів. Ви можете розділити контексти, щоб ізолювати мінливість, але в такому разі ви вручну створюєте обхідні шляхи для оптимізації, які інший інструмент уже вирішує.
Коли Redux Toolkit виправдовує своє місце
Redux Toolkit розроблений для застосунків, де стан є складним, оновлення відбуваються часто, а кілька віддалених функцій мають читати та записувати ті самі дані, не конфліктуючи між собою. Розглянемо кошик для покупок. Користувач додає товар із картки товару. Іконка кошика в хедері має оновити лічильник. Висувається бічна панель, щоб показати перелік товарів. Поле введення промокоду виконує валідацію. Сторінка оформлення замовлення пізніше зчитує вміст кошика. Цей стан використовується непов'язаними компонентами по всьому дереву, і він часто змінюється.
Redux Toolkit вирішує це за допомогою централізованого сховища (store) та явних фрагментів стану (slices). Компоненти підписуються лише на ті частини даних, які їм потрібні, використовуючи useSelector. Якщо ціна акції оновлюється в дашборді в реальному часі, компонент, що відображає налаштування профілю користувача, не активується. Redux використовує перевірки рівності за посиланням (reference equality checks) «під капотом», щоб підписки були гранулярними. Це стає критично важливим, коли кількість ваших компонентів сягає сотень.
Redux також забезпечує передбачуваний потік даних. Зміни стану відбуваються через відправлені (dispatched) дії (actions), які обробляються редюсерами (reducers). Це звучить як жаргон, але на практиці це означає, що ви можете виконати grep по коду для addToCart і знайти кожен шлях виконання, який змінює кошик. У великій команді такий «контракт» запобігає помилкам. Context, навпаки, — це просто значення та сеттер. Будь-який споживач може викликати setState, і пошук джерела неправильного значення означає встановлення точок зупинки (breakpoints) у багатьох компонентах.
Де вони насправді розходяться
Характеристики продуктивності відрізняють ці інструменти більше, ніж будь-що інше. Context транслює нове значення всім споживачам без жодних умов. Redux сповіщає лише тих підписників, чий вибраний slice змінився. Якщо ви створюєте біржовий дашборд у реальному часі, де котирування оновлюються щосекунди, Context спровокує шторм глобальних ререндерів. Redux дозволить перерахувати лише комірку тікера та спарклайн-графік.
Відлагодження (Debugging) — це ще одна сфера, де Redux випереджає конкурента у складних застосунках. Redux DevTools надає можливість відлагодження з «подорожжю в часі» (time-travel debugging). Ви можете повертатися назад через кожну виконану (dispatched) дію та спостерігати, як стан відкочується назад. У багатоетапному процесі оформлення замовлення з розрахунком доставки, валідацією платежу та відновленням після помилок, можливість відтворити саме ту послідовність, яка призвела до багу, є безцінною. Context покладається на стандартні React DevTools. Ви можете переглядати поточні значення context, але в ньому немає вбудованого журналу дій або переглядача різниці станів (state diff viewer). Вам доведеться знову розкидати console.log по всьому коду.
Middleware та побічні ефекти є частиною ДНК Redux. Redux Toolkit включає createAsyncThunk і гарно інтегрується з бібліотеками для отримання даних. Ви можете організувати виклик API, показати спінер завантаження, обробити збій мережі та закешувати результат — і все це в межах потоку даних Redux. Context не пропонує вбудованого патерну для асинхронної логіки. Ви або отримуєте дані всередині компонентів, а потім передаєте результат у Context, або обгортаєте провайдери у саморобні асинхронні утиліти. Це працює, але це рішення ad hoc.
Вартість налаштування — це те, де Context перемагає без заперечень. Створення theme provider займає близько п'яти хвилин. Redux Toolkit вимагає створення файлу store, визначення slices та обгортання вашого застосунку в Provider. Це вже не та тривала церемонія, якою це було зі старим Redux та його горами шаблонного коду (boilerplate), але це все одно складніше налаштування, ніж у Context. Для сайд-проєкту на вихідні або дашборду з трьома маршрутами такі витрати зусиль можуть бути невиправданими.
Використання обох інструментів в одному застосунку
Вам не обов'язково присягати на вірність лише одному табору. Багато робочих застосунків використовують Context для глобальних питань UI-оболонки, а Redux — для важких бізнес-даних домену. Поширений патерн полягає в тому, щоб тримати тему, локаль і, можливо, легкий прапорець авторизації в Context, оскільки вони потрібні на кожному маршруті та змінюються рідко. Водночас система управління замовленнями, центр сповіщень і таблиці даних живуть у Redux, де часті оновлення та міжкомпонентна логіка вимагають точного контролю.
Такий гібридний підхід дозволяє простим речам залишатися простими, не змушуючи вас створювати повноцінний Redux store навколо статичного об'єкта теми. Це також запобігає заповненню ваших Redux slices елементами інтерфейсу (UI chrome), які від самого початку не потребували професійного управління станом промислового рівня.
Головний висновок
Вибір важчого інструмента не є ознакою майстерності. Почніть із аналізу того, як часто змінюється ваш стан, скільки компонентів його використовують і чи потрібно вам відстежувати мутації між різними частинами команди. Якщо ви керуєте значеннями, що змінюються повільно і використовуються повсюдно в застосунку середнього розміру, Context, ймовірно, буде достатньо. Якщо ваш стан змінюється часто, охоплює непов'язані функції та потребує чіткого журналу змін (audit trail), Redux Toolkit вбереже вас від зайвого болю.
Обирайте інструмент відповідно до структури вашого проєкту, а не на основі доповідей на конференціях чи зірок на GitHub. Кошик для покупок на п'ятдесят товарів не вимагає автоматичного використання Redux, а перемикач теми не потребує глобального store. Підбирайте інструмент під задачу, і ваш кодовий базис залишатиметься придатним для підтримки ще довго після того, як хайп навколо цих технологій вщухне.
