Context API
React Context API — радіовежа на пагорбі транслює сигнал одночасно до багатьох приймачів
У попередніх уроках ми передавали дані від батьківських компонентів до дочірніх через props. Це чудово працює для 1-2 рівнів. Але що, якщо потрібно передати тему (dark/light) або дані авторизованого користувача через 5-10 рівнів компонентів? Тут на допомогу приходить Context API.
Проблема: prop drilling
Prop drilling -- це коли props передаються через багато проміжних компонентів, які самі ці дані не використовують:
// App → Layout → Sidebar → UserMenu → UserAvatar
// theme потрібен тільки в UserAvatar, але проходить через усі рівні
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return <Layout theme={theme} setTheme={setTheme} />
}
function Layout({ theme, setTheme }: LayoutProps) {
// Layout не використовує theme, але передає далі
return <Sidebar theme={theme} setTheme={setTheme} />
}
function Sidebar({ theme, setTheme }: SidebarProps) {
// Sidebar теж не використовує, але передає далі
return <UserMenu theme={theme} setTheme={setTheme} />
}
function UserMenu({ theme, setTheme }: UserMenuProps) {
// Нарешті використовуємо!
return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Тема: {theme}
</button>
}
Це незручно: додавати новий prop означає змінювати кожен проміжний компонент.
Context -- рішення
Context дозволяє "телепортувати" дані від батьківського компонента до будь-якого дочірнього, незалежно від глибини:
App (Provider)
└── Layout
└── Sidebar
└── UserMenu (useContext) ← отримує дані напряму!
Створення Context: три кроки
Крок 1: Створити Context
// src/contexts/ThemeContext.tsx
import { createContext } from 'react'
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
export const ThemeContext = createContext<ThemeContextType | null>(null)
Крок 2: Обгорнути у Provider
Provider -- компонент, що "роздає" дані всім дочірнім:
// src/contexts/ThemeContext.tsx (продовження)
import { createContext, useState } from 'react'
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
export const ThemeContext = createContext<ThemeContextType | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
function toggleTheme() {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// src/App.tsx
import { ThemeProvider } from './contexts/ThemeContext'
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
)
}
Крок 3: Використати useContext
import { useContext } from 'react'
import { ThemeContext } from '../contexts/ThemeContext'
function UserMenu() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('UserMenu має бути всередині ThemeProvider')
}
const { theme, toggleTheme } = context
return (
<div className={`menu menu-${theme}`}>
<button onClick={toggleTheme}>
Тема: {theme === 'light' ? 'Світла' : 'Темна'}
</button>
</div>
)
}
Custom hook для Context
Перевірку на null доводиться писати щоразу. Створимо custom hook, який робить це автоматично:
// src/contexts/ThemeContext.tsx
import { createContext, useContext, useState } from 'react'
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
// Custom hook
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme має використовуватись всередині ThemeProvider')
}
return context
}
// Provider
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
function toggleTheme() {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
Тепер використання стає простим:
import { useTheme } from '../contexts/ThemeContext'
function UserMenu() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
{theme === 'light' ? 'Увімкнути темну тему' : 'Увімкнути світлу тему'}
</button>
)
}
Цей патерн -- createContext + Provider компонент + custom hook -- стандартний підхід у React. Ти будеш використовувати його постійно. Context не експортується напряму -- тільки hook та Provider.
Практика: ThemeContext з localStorage
Повний приклад з збереженням теми:
// src/contexts/ThemeContext.tsx
import { createContext, useContext, useState, useEffect } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme має використовуватись всередині ThemeProvider')
}
return context
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
// Ініціалізація з localStorage
const saved = localStorage.getItem('theme')
return (saved === 'dark' || saved === 'light') ? saved : 'light'
})
useEffect(() => {
localStorage.setItem('theme', theme)
// Застосувати тему до body
document.body.className = theme
}, [theme])
function toggleTheme() {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// src/components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext'
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme} className="theme-toggle">
{theme === 'light' ? 'Темна тема' : 'Світла тема'}
</button>
)
}
// src/components/Header.tsx
import { useTheme } from '../contexts/ThemeContext'
import ThemeToggle from './ThemeToggle'
export default function Header() {
const { theme } = useTheme()
return (
<header className={`header header-${theme}`}>
<h1>Мій додаток</h1>
<ThemeToggle />
</header>
)
}
Кілька Context разом
Можна мати стільки контекстів, скільки потрібно:
// src/App.tsx
function App() {
return (
<ThemeProvider>
<AuthProvider>
<Layout />
</AuthProvider>
</ThemeProvider>
)
}
// Будь-який компонент може використовувати обидва:
function Dashboard() {
const { theme } = useTheme()
const { user } = useAuth()
return (
<div className={`dashboard dashboard-${theme}`}>
<h1>Привіт, {user?.name}!</h1>
</div>
)
}
Коли Context, а коли props?
| Ситуація | Що використати |
|---|---|
| Дані потрібні 1-2 рівні нижче | Props |
| Дані потрібні глибоко в дереві | Context |
| Глобальні дані (тема, мова, авторизація) | Context |
| Дані специфічні для конкретного компонента | Props |
| Список елементів з callback | Props |
Context не замінює props повністю. Якщо дані передаються на 1-2 рівні -- props простіший та зрозуміліший. Context -- для дійсно глобальних даних, що потрібні багатьом компонентам на різних рівнях.
Підсумок
- Prop drilling -- передача props через багато рівнів, що не використовують ці дані
- Context -- "телепортує" дані від Provider до будь-якого дочірнього компонента
- Три кроки:
createContext→Provider→useContext - Custom hook (наприклад
useTheme) -- стандартний патерн для Context - Зберігайте контексти в окремих файлах (
src/contexts/) - Context для глобальних даних (тема, авторизація, мова), props -- для локальних
Що далі?
У наступному уроці навчимось додавати навігацію між сторінками за допомогою React Router -- routing для single-page додатків.
Корисні посилання:
- React.dev: Passing Data Deeply with Context -- повний гайд
- React.dev: useContext -- API-довідник
- React.dev: Scaling Up with Reducer and Context -- Context + useReducer