Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке context та хук useContext у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React Context** - це спосіб передавати дані по дереву компонентів без prop drilling. `useContext` читає спільне значення всередині будь-якого функціонального компонента. При зміні значення Provider усі компоненти, що викликали `useContext` для цього контексту, перерендеряться автоматично. ```jsx const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Child /> </ThemeContext.Provider> ); } function Child() { const theme = useContext(ThemeContext); // 'dark' return <div className={theme}>Привіт</div>; } ``` **Головне:** будь-який нащадок читає значення напряму. Проміжні компоненти нічого не передають.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.