Що таке context та хук useContext у React
React Context - це вбудований механізм для передачі даних по дереву компонентів без необхідності пробрасовувати пропси через кожен проміжний рівень. useContext - це хук, який читає ці спільні дані всередині будь-якого функціонального компонента.
Теорія
TL;DR
- Context - як корпоративна дошка оголошень: публікуєш значення один раз у Provider зверху, і будь-який компонент бере його через
useContextбез зайвих передач через проміжні компоненти. - Головне: Context прибирає prop drilling. Компоненти підписуються і автоматично оновлюються при зміні значення.
- Використовуй, якщо 3 і більше компонентів потребують однакових даних. Для 1-2 рівнів простіші пропси.
createContextстворює об'єкт контексту;Providerзадає значення;useContextйого читає.- Дефолтне значення, передане в
createContext, активується тільки якщо Provider відсутній у дереві вище.
Швидкий приклад
import { createContext, useContext } from 'react';
// Створюємо один раз, поза компонентами
const ThemeContext = createContext('light'); // 'light' - запасний дефолт
function App() {
return (
// Provider задає значення для всього піддерева
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <div><Button /></div>; // Нічого не приймає і не передає
}
function Button() {
const theme = useContext(ThemeContext); // Читає 'dark' напряму
return <button className={theme}>Я {theme}</button>;
}Button отримує "dark" напряму з контексту. Toolbar нічого не знає про тему. Жодного ланцюжка пропсів.
Головна відмінність від пропсів
Пропси - це явна передача від батька до дитини, рівень за рівнем. Кожен компонент у ланцюжку мусить пробросити значення далі, навіть якщо сам його не використовує. Context прибирає цей ланцюжок. Provider поміщає значення у спільний канал, і будь-який нащадок читає його через useContext напряму, незалежно від глибини. Проміжні компоненти залишаються чистими.
Коли використовувати
- Одні й ті самі дані потрібні багатьом компонентам (авторизація, тема, локаль): Context.
- Дані передаються лише на 1-2 рівні: пропси. Простіше відстежити.
- Часті оновлення глибоко в дереві: Context разом з
useReducer. - Складний глобальний стан з багатьма діями: варто розглянути Redux або Zustand. Context не має вбудованих devtools, middleware чи підтримки селекторів.
Як React знаходить значення
Коли викликаєш useContext(ThemeContext), React піднімається по fiber-дереву від компонента, що викликав хук, і знаходить найближчий ThemeContext.Provider. Результат кешується у fiber-вузлі цього компонента окремо для кожного типу контексту. У React 18 при зміні значення Provider React планує перерендер тільки для підписаних споживачів через окрему чергу диспетчера. Весь дерево не перемальовується - тільки ті компоненти, які викликали useContext для цього конкретного контексту.
Типові помилки
Помилка 1: Створення контексту всередині компонента
// Неправильно: новий об'єкт контексту на кожен рендер
function App() {
const MyContext = createContext(); // Ламає мемоізацію всіх споживачів
return <MyContext.Provider value={data}>...</MyContext.Provider>;
}
// Правильно: оголошуємо поза компонентом
const MyContext = createContext();
function App() {
return <MyContext.Provider value={data}>...</MyContext.Provider>;
}Кожен рендер створює новий об'єкт контексту, що змушує всіх підписників перерендеритися, навіть якщо дані не змінились.
Помилка 2: Пряма мутація значення контексту
// Неправильно: мутуємо спільне посилання на об'єкт
const config = { theme: 'light' };
<Provider value={config}>...</Provider>
// Десь у дочірньому компоненті:
config.theme = 'dark'; // Інші споживачі бачать зміну несподіваноReact очікує нові посилання на об'єкти, щоб тригернути перерендер. Мутація обходить цей механізм, і оновлення стають непередбачуваними. Передавай функцію-сеттер: value={{ theme, setTheme }}.
Помилка 3: Неправильний порядок вкладення Provider-ів
// Переплутаний порядок: useContext(AuthContext) може отримати не те значення
<ThemeProvider>
<AuthProvider>
<App /> {/* useContext(AuthContext) тут резолвиться коректно */}
</AuthProvider>
</ThemeProvider>useContext знаходить найближчий Provider, піднімаючись вгору. При неправильному вкладенні компонент тихо отримує дефолтне значення замість реального. Цей баг виявляється тільки в рантаймі, і першого разу я витратив на нього добрих пів години.
Помилка 4: Context для всього підряд
Context - це не менеджер стану. Якщо помістити туди значення, яке часто змінюється (наприклад, поточний текст у полі вводу), кожен підписник перерендериться при кожному натисканні клавіші. Для таких випадків підходить локальний стан або спеціалізований стор.
Помилка 5: Відсутнє дефолтне значення
const AuthContext = createContext(); // undefined за замовчуванням
// Компонент поза Provider:
function Orphan() {
const { user } = useContext(AuthContext); // TypeError: cannot destructure undefined
}Завжди задавай осмислене дефолтне значення або хоча б createContext(null) і перевіряй на null у споживачах.
Де зустрічається в реальних проектах
- Next.js / next-auth:
SessionProviderогортає весь застосунок і надає дані сесії. Будь-яка сторінка читає їх черезuseSession(), який використовує контекст всередині. - Chakra UI / shadcn/ui:
ColorModeProviderдля токенів теми. Компоненти читають активний колірний режим без пропсів. - React Router:
RouterContextпід капотом. Кожен викликuseParams()іuseNavigate()читає його. - Vercel dashboard (публічні доповіді про архітектуру):
UserContextдля перемикання організацій між навігацією і сайдбаром.
Питання на співбесіді
Q: Що повертає useContext, якщо компонент знаходиться поза Provider?
A: Дефолтне значення, передане у createContext. Без помилок, без краша. Тому осмислений дефолт важливіший, ніж здається.
Q: Чи кожна зміна значення контексту перерендерить усіх споживачів?
A: Так, за замовчуванням. Якщо Provider отримує новий об'єкт на кожен рендер батька (наприклад, value={{ user }}), усі підписники перерендеряться. Обгортай значення у useMemo, щоб стабілізувати посилання.
Q: Чим useContext відрізняється від старого Context.Consumer?
A: Consumer використовує render prop і працює в класових та функціональних компонентах. useContext - це хук, тільки для функціональних компонентів. Код стає значно чистішим.
Q: Коли поєднувати Context з useReducer?
A: Коли спільний стан має кілька варіантів оновлення (логін, логаут, зміна ролі). useReducer тримає логіку організованою. Ти передаєш { state, dispatch } як значення контексту - це паттерн, який Ден Абрамов описав як правильний спосіб масштабування Context без переходу на зовнішній стор.
Q: Чому великі застосунки вибирають Redux або Zustand замість Context?
A: Context не має devtools, middleware і підтримки селекторів. Не можна підписатися тільки на частину стану, тому будь-яка зміна перерендерить усіх споживачів. Як тільки з'являється 10+ дій і потреба в налагодженні, виправданіше взяти спеціалізований стор.
Q: Як startTransition у React 18 взаємодіє з оновленнями контексту?
A: Обгорни зміну значення Provider у startTransition, і React позначить цей перерендер як некритичний. Це дозволяє зберегти відгук інтерфейсу під час важких оновлень.
Приклади
Базовий: Provider теми
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
function Page() {
return <Header />; // Нічого не приймає і не передає
}
function Header() {
const theme = useContext(ThemeContext); // 'dark'
return <header className={theme}>Шапка сайту</header>;
}Page нічого не знає про тему. Header читає її напряму. Саме так влаштований ColorModeProvider у Chakra UI і shadcn/ui для дизайн-токенів.
Середній: контекст авторизації з діями
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState({ name: 'Alex', id: 1 });
return (
<AuthContext.Provider value={{ user, login: setUser }}>
{children}
</AuthContext.Provider>
);
}
function Profile() {
const { user, login } = useContext(AuthContext);
return (
<div>
<p>Вітаємо, {user.name}!</p> {/* Показує 'Alex' */}
<button onClick={() => login({ name: 'Bob', id: 2 })}>
Змінити користувача
</button>
</div>
);
}
function App() {
return (
<AuthProvider>
<Profile />
</AuthProvider>
);
}Натискаєш кнопку - перерендериться тільки Profile. Компоненти, які не викликають useContext(AuthContext), не зачіпаються. Це той самий паттерн, що використовує SessionProvider у next-auth.
Просунутий: дефолтне значення і вкладені Provider-и
const ThemeContext = createContext('light'); // Дефолт при відсутньому Provider
function App() {
return (
<ThemeContext.Provider value="dark">
<Nested />
</ThemeContext.Provider>
);
}
function Nested() {
return <DeepChild />;
}
function DeepChild() {
const theme = useContext(ThemeContext); // 'dark' від Provider в App
return <span>{theme}</span>;
}
// Компонент поза деревом Provider:
function Orphan() {
const theme = useContext(ThemeContext); // 'light' (дефолт), без помилок
return <span>{theme}</span>;
}DeepChild отримує "dark". Orphan отримує "light". React не кидає помилку при відсутності Provider - просто повертає дефолтне значення. Саме тому правильний дефолт важливий для graceful degradation.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.