useContext.Ми знаємо, що стан у React прив’язаний до компонента, де викликаний хук useState. Якщо нам потрібно передати цей стан іншим компонентам, ми спускаємо його “вниз по дереву” через Пропси (Props) від Батька до Дітей.
Але що робити, якщо нашому додатку потрібні дані про поточного користувача (його аватарку чи статус авторизації) на абсолютно кожній сторінці? Аватарка має показуватися в <Header>, у вікні <ProfileSettings>, і біля кожного залишеного коментаря у <CommentBlock>.
Передавати об’єкт user через Props через десятки посередників, яким цей об’єкт насправді не потрібен — це жахливий архітектурний біль.
Для вирішення цієї проблеми були розроблені інструменти управління Глобальним станом (State Managers). Мета цієї лекції — розібрати вбудований інструмент React Context API, який підходить для простих речей (як темна/світла тема), та познайомитися із сучасною, легкою альтернативою — бібліотекою Zustand.
Уявіть, що у нас є компонент App (Батько). Всередині нього є Layout, всередині нього — MainContent, потім Article, потім ArticleFooter, і аж всередині ArticleFooter знаходиться компонент AuthorAvatar, якому потрібне посилання на картинку користувача user.avatar.
// Щоб передати дані в AuthorAvatar, нам доведеться прокинути їх через ВСІХ посередників
<App>
<Layout user={user}>
<MainContent user={user}>
<Article user={user}>
<ArticleFooter user={user}>
<AuthorAvatar src={user.avatar} />{" "}
{/* ТІЛЬКИ ТУТ він реально потрібен! */}
</ArticleFooter>
</Article>
</MainContent>
</Layout>
</App>
Ця проблема, коли ви змушені передавати Пропси глибоко по дереву через компоненти, яким ці пропси не потрібні (вони працюють просто як кур’єри), називається Prop Drilling. Це засмічує код, ускладнює його читання і робить рефакторинг неможливим.
useState): Це дані, які потрібні ТІЛЬКИ одному компоненту або його найближчим сусідам. Наприклад: відкрите чи закрите модальне вікно, поточний текст в інпуті форми Пошуку. Не треба тягнути їх глобально!user, token).Щоб вирішити проблему Prop Drilling без використання сторонніх бібліотек, React пропонує власний інструмент — Context API.
Він працює як своєрідний “тунель” або “портал”: ви кладете дані в обгортку на самій вершині App (Провайдер), а будь-який компонент на будь-якій глибині може одягнути “навушники” (Споживач) і миттєво почути ці дані.
Спочатку ми створюємо файл для нашого контексту (наприклад ThemeContext.jsx). За допомогою вбудованої функції createContext.
import { createContext } from "react";
// Створюємо порожню коробку-контекст.
// Можна вказати дефолтне значення (напр. 'light'), що буде використано, якщо не буде Провайдера
export const ThemeContext = createContext("light");
Щоб наповнити цю коробку реальними даними, які будуть змінюватись (useState), ми робимо компонент обгортку:
// файл ThemeContext.jsx
import { createContext, useState } from "react";
export const ThemeContext = createContext(); // Експортуємо саму "коробку"
// Це наш кастомний компонент-обгортка
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
// Все, що ми покладемо в атрибут `value`, стане доступним глобально
return (
<ThemeContext.Provider value=>
{/* Всі дитячі компоненти додатка */}
{children}
</ThemeContext.Provider>
);
}
В найвищому файлі main.jsx ми огортаємо весь додаток цією коробкою:
<ThemeProvider>
<App />
</ThemeProvider>
useContextТепер кнопка, що лежить десь дуже глибоко в Header, може спожити цей контекст і отримати дані напряму без Props!
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext"; // Імпортуємо ту саму "коробку"
function ThemeSwitcherButton() {
// Магія: отримуємо змінні з "порталу"
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
{theme === "light" ? "🌚 Зробити Темним" : "🌞 Зробити Світлим"}
</button>
);
}
Для простих речей (Тема, Мова, Користувач) Context підходить ідеально. Але для великих обсягів даних, що часто змінюються (як список постійно оновлюваних котирувань акцій чи великий складний кошик), він має серйозну архітектурну проблему — Проблему зайвих рендерів.
Якщо ви зміните хоча б одну маленьку властивість з 50-ти, що лежать у значенні (value) Provider-а, УСІ компоненти, які споживають цей контекст, перемалюються повністю, навіть якщо їм конкретно ця маленька властивість не була потрібна. Додаток може почати гальмувати.
Раніше для складних задач абсолютно всі використовували Redux. Але Redux мав настільки “страшний” і багатослівний код налаштування (Boilerplate), що всі його ненавиділи.
Тому сьогодні індустрія перейшла до легких і потужних Стейт-менеджерів, і найпопулярнішим серед них зараз є Zustand (“стан” німецькою).
Zustand — це крихітна стороння бібліотека, яка вирішує всі проблеми Контексту. Її головні плюси:
Встановлення:
npm install zustand
Замість “провайдера і контексту” ми створюємо один центральний Store (склад) за допомогою функції create. В ньому будуть жити як дані (state), так і функції для їхньої зміни (actions).
// файл store/cartStore.js
import { create } from "zustand";
export const useCartStore = create((set) => ({
// 1. Початковий стан
cartCount: 0,
items: [],
// 2. Функції зміни стану (Actions)
// Функція set оновлює конкретне поле і зливає його зі старим станом
addToCart: (productName) => {
set((state) => ({
cartCount: state.cartCount + 1,
items: [...state.items, productName],
}));
},
clearCart: () => set({ cartCount: 0, items: [] }),
}));
Нашому компоненту не потрібен доступ до всього Сховища. Він отримує свій власний хук, і витягує з нього ЛИШЕ те, що треба.
// Компонент Хедера (потрібен тільки лічильник)
import { useCartStore } from "../store/cartStore";
function HeaderBlock() {
// Компонент Хедера підписується ТІЛЬКИ на `cartCount`.
// Якщо зміняться `items`, він не буде перемальовуватися! Ось в чому сила Zustand.
const cartCount = useCartStore((state) => state.cartCount);
return <div>Кошик: {cartCount} товарів</div>;
}
// Компонент Купівлі (потрібна функція додавання)
function BuyButton({ product }) {
// Цей компонент дістав тільки функцію (Action)
const addToCart = useCartStore((state) => state.addToCart);
return <button onClick={() => addToCart(product.name)}>Купити</button>;
}
Все! Ніяких Провайдерів у App.jsx, ніяких зайвих перемальовок. Zustand робить глобальний стан чистим і неймовірно швидким.
useState) слід використовувати для локальних даних UI-елементів. Концепція Global State необхідна лише тоді, коли однаковими даними паралельно користуються далекі компоненти, щоб уникнути проблеми “свердління пропсів” (Prop Drilling).<MyContext.Provider value={...}>, і використовувати інструмент-приймач useContext(MyContext) будь-де нижче в ієрархії.Prop Drilling.useState), а які доцільно винести на Глобальний рівень?<ThemeContext.Provider>, навіщо йому атрибут value?Zustand над класичним рішенням з Context API, коли мова заходить про швидке розгортання проєкту.