Skip to main content

Що таке context та хук useContext у React

React Context - це вбудований механізм для передачі даних по дереву компонентів без необхідності пробрасовувати пропси через кожен проміжний рівень. useContext - це хук, який читає ці спільні дані всередині будь-якого функціонального компонента.

Теорія

TL;DR

  • Context - як корпоративна дошка оголошень: публікуєш значення один раз у Provider зверху, і будь-який компонент бере його через useContext без зайвих передач через проміжні компоненти.
  • Головне: Context прибирає prop drilling. Компоненти підписуються і автоматично оновлюються при зміні значення.
  • Використовуй, якщо 3 і більше компонентів потребують однакових даних. Для 1-2 рівнів простіші пропси.
  • createContext створює об'єкт контексту; Provider задає значення; useContext його читає.
  • Дефолтне значення, передане в createContext, активується тільки якщо Provider відсутній у дереві вище.

Швидкий приклад

jsx
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: Створення контексту всередині компонента

jsx
// Неправильно: новий об'єкт контексту на кожен рендер 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: Пряма мутація значення контексту

jsx
// Неправильно: мутуємо спільне посилання на об'єкт const config = { theme: 'light' }; <Provider value={config}>...</Provider> // Десь у дочірньому компоненті: config.theme = 'dark'; // Інші споживачі бачать зміну несподівано

React очікує нові посилання на об'єкти, щоб тригернути перерендер. Мутація обходить цей механізм, і оновлення стають непередбачуваними. Передавай функцію-сеттер: value={{ theme, setTheme }}.

Помилка 3: Неправильний порядок вкладення Provider-ів

jsx
// Переплутаний порядок: useContext(AuthContext) може отримати не те значення <ThemeProvider> <AuthProvider> <App /> {/* useContext(AuthContext) тут резолвиться коректно */} </AuthProvider> </ThemeProvider>

useContext знаходить найближчий Provider, піднімаючись вгору. При неправильному вкладенні компонент тихо отримує дефолтне значення замість реального. Цей баг виявляється тільки в рантаймі, і першого разу я витратив на нього добрих пів години.

Помилка 4: Context для всього підряд

Context - це не менеджер стану. Якщо помістити туди значення, яке часто змінюється (наприклад, поточний текст у полі вводу), кожен підписник перерендериться при кожному натисканні клавіші. Для таких випадків підходить локальний стан або спеціалізований стор.

Помилка 5: Відсутнє дефолтне значення

jsx
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 теми

jsx
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 для дизайн-токенів.

Середній: контекст авторизації з діями

jsx
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-и

jsx
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?