useState стає недостатньо.createContext, Provider та хук useContext.Вирішення проблеми “prop drilling” (прокидання пропсів через багато рівнів). Створення контексту, використання Provider та хука useContext. Обговорюються обмеження контексту в контексті частого оновлення даних та сценарії, коли варто використовувати зовнішні бібліотеки керування станом, такі як Redux або Zustand.
У попередніх лекціях ми встановили, що стан компонента — це локальна пам’ять, а пропси (Props) забезпечують односпрямований потік даних вниз по дереву. Для простих додатків цієї архітектури достатньо. Однак, на масштабах ентерпрайз-додатків інженери стикаються з необхідністю доступу до одних і тих самих даних (наприклад, інформації про авторизованого користувача, теми оформлення, кошика товарів) з десятків різних компонентів, що знаходяться на різних гілках дерева і рознесені на багато рівнів вкладеності. Ця лекція детально аналізує вбудований у React механізм Context API для обходу дерева компонентів та розглядає межу, після якої необхідні зовнішні State Management бібліотеки.
В архітектурі React зміна стану батька передається дітям через пропси. Якщо компонент А має стан, який потрібен компоненту D (який вкладений так: A -> B -> C -> D), нам доведеться передати цей стейт як пропс через компоненти B і C.
const Root = () => {
const [theme, setTheme] = useState("dark");
// Компонент Layout нічого не знає про theme, але мусить його прийняти і передати далі
return <Layout theme={theme} />;
};
const Layout = ({ theme }) => <Header theme={theme} />;
const Header = ({ theme }) => <ProfileButton theme={theme} />;
const ProfileButton = ({ theme }) => <button className={theme}>Профіль</button>;
Це явище називається Prop Drilling (Прокидання/Буріння пропсів).
Для вирішення проблеми Prop Drilling, починаючи з React 16.3, був стабілізований Context API. Це класичний патерн “Видавець-Підписник” (Publisher-Subscriber / Dependency Injection), вбудований у дерево React.
Механізм складається з 3 етапів:
1. Створення “труби” (Контексту):
import { createContext } from "react";
// 'light' - резервне значення, якщо компонент викличе useContext без Провайдера вище
export const ThemeContext = createContext("light");
2. Надання даних (Provider): Провайдер — це компонент, який “наповнює трубу” актуальними даними.
const App = () => {
const [theme, setTheme] = useState("dark");
return (
// Всі дочірні компоненти, на будь-якій глибині, отримають доступ до value
<ThemeContext.Provider value={theme}>
<Layout />
</ThemeContext.Provider>
);
};
3. Споживання даних (Хук useContext):
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
const ProfileButton = () => {
// Мигдаємо посередників: прямий доступ до значення з найближчого Провайдера вгорі!
const theme = useContext(ThemeContext);
return <button className={theme}>Профіль</button>;
};
Контекст виглядає як “срібна куля”, але має дуже високу архітектурну ціну.
Головне правило Context API: Кожного разу, коли властивість value у Provider змінюється (за посиланням або за значенням), React ПРИМУСОВО рендерить УСІХ споживачів (useContext) цього контексту.
Цей процес ігнорує будь-які оптимізації React.memo.
// Глобальний надмірний контекст (Антипатерн)
const AppContext = createContext();
const AppProvider = ({ children }) => {
const [theme, setTheme] = useState("dark");
const [user, setUser] = useState({ name: "Alex" });
// Коли ми робимо setTheme('light'), створюється новий об'єкт value (Referential Equality = false!).
return (
<AppContext.Provider value=>
{children}
</AppContext.Provider>
);
};
У цьому прикладі, якщо зміниться ТІЛЬКИ theme, компонент ProfileAvatar, який підписаний на AppContext лише заради user, буде перерендерений, оскільки об’єкт value змінив своє посилання. Це призводить до катастрофічних втрат FPS на великих сторінках.
Щоб вирішити проблему зайвих рендерів, системні інженери застосовують Split Contexts. Ми не створюємо один “божественний” (God Object) контекст для всього відразу. Ми розділяємо контексти за напрямками частоти оновлень або за сутностями.
Більше того, гарною практикою є розділення контексту Даних (що змінюються часто) та контексту Дій/Методів (які не змінюються ніколи).
const ThemeStateContext = createContext("light");
const ThemeDispatchContext = createContext(() => {}); // Функції зміни
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("dark");
return (
<ThemeStateContext.Provider value={theme}>
{/* useCallback гарантує, що посилання на setTheme не змінюється */}
<ThemeDispatchContext.Provider value={setTheme}>
{children}
</ThemeDispatchContext.Provider>
</ThemeStateContext.Provider>
);
};
Тепер компонент ThemeToggleButton, який потребує ТІЛЬКИ функції зміни (Dispatch), підписується на ThemeDispatchContext. При зміні теми, цей компонент не буде перерендерений, оскільки значення ThemeDispatchContext не змінилося!
Серед розробників побутує оманлива думка, що хуки (useContext + useReducer) “вбили” Redux. Це фундаментально некоректно.
Context API — це НЕ інструмент управління станом (State Management).
Контекст — це інструмент Dependency Injection (Доставки даних). Сам по собі Контекст нічого не зберігає (зберігає useState всередині Провайдера).
Обмеження Context API:
user.firstName, але проігноруй зміну user.lastName”.Context ідеальний для: Тема (Dark/Light), Активний користувач (Auth Session), Мова інтерфейсу (i18n), Налаштування конфігурації (Config). Тобто для даних, що змінюються вкрай рідко.
Коли додаток стабільно зростає, розробники впроваджують інструменти Глобального Управління Станом (Global State Management). Ці інструменти зберігають дані поза деревом React і мають власні алгоритми підписки (Pub/Sub), що дозволяють оновлювати компоненти точково.
Світовий стандарт (Enterprise). Реалізує патерн Flux (Диспетчер -> Екшен -> Редюсер -> Стор).
Модерн-стандарт. Мінімалістична бібліотека від творців Jotai та React Spring.
import create from "zustand";
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}));
// У компоненті ми підписуємось ТІЛЬКИ на поле bears. Зміна інших полів цей компонент не зачепить!
const bears = useBearStore((state) => state.bears);
Представник парадигми Observer/Observable (ближче до ООП). Використовує мутабельний підхід (всупереч філософії React), але завдяки Proxy-об’єктам під капотом працює блискавично швидко і магічним чином оновлює тільки те, що потрібно. Поступово втрачає популярність на користь Zustand/RTK через специфічність.
useContext.value. Це робить Контекст непридатним для даних, що змінюються з високою частотою.<Context.Provider value=> є критичним антипатерном при масштабному застосуванні?StateContext та DispatchContext)?