Представьте дерево компонентов: App → Layout → Sidebar → UserAvatar. Если UserAvatar нужен текущий пользователь, придётся передавать user через Layout и Sidebar, хотя им самим он не нужен. Это называется prop drilling — "бурение пропсов".
// Проблема: Layout и Sidebar только "прокидывают" user, не используя его
function App() {
const user = { name: 'Алексей' }
return <Layout user={user} />
}
function Layout({ user }) {
return <Sidebar user={user} /> // Layout не использует user!
}
function Sidebar({ user }) {
return <UserAvatar user={user} /> // Sidebar тоже!
}
function UserAvatar({ user }) {
return <img alt={user.name} /> // Только здесь user нужен
}Context позволяет «прокинуть» данные через дерево без явной передачи через пропсы:
// 1. Создаём контекст с типом по умолчанию
const UserContext = React.createContext(null)
// 2. Оборачиваем дерево в Provider
function App() {
const user = { name: 'Алексей' }
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
)
}
// 3. Читаем из любого дочернего компонента
function UserAvatar() {
const user = useContext(UserContext)
return <img alt={user.name} />
}Context хорошо подходит для:
Context не подходит для часто меняющихся данных (каждый потребитель перерисуется), высокочастотных обновлений (счётчики, позиция мыши) и сложной логики с множеством действий (лучше Zustand/Redux).
Рекомендуется оборачивать логику провайдера в отдельный компонент и экспортировать кастомный хук:
// theme-context.jsx
const ThemeContext = createContext(null)
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
// Кастомный хук — защита от использования вне провайдера
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme должен использоваться внутри ThemeProvider')
return ctx
}Главный подводный камень: при изменении значения контекста все потребители (useContext) перерисовываются, даже если использованное ими поле не изменилось.
// Решение 1: разделить на несколько контекстов
const UserContext = createContext(null)
const ThemeContext = createContext(null)
// Решение 2: стабилизировать value через useMemo
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const value = useMemo(() => ({ user, theme }), [user, theme])
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}Можно вкладывать провайдеры — каждый компонент читает нужный:
function App() {
return (
<ThemeProvider>
<AuthProvider>
<LocaleProvider>
<Router />
</LocaleProvider>
</AuthProvider>
</ThemeProvider>
)
}| | Context | Zustand | Redux |
|---|---|---|---|
| Настройка | Минимальная | Минимальная | Много бойлерплейта |
| Производительность | Перерисовывает всех | Точечные обновления | Точечные обновления |
| DevTools | Нет | Есть (middleware) | Отличные |
| Подходит для | Статичные данные | Средние приложения | Большие команды |
Реализация Context API с нуля через паттерн pub/sub: createContext, Provider, useContext — без React
// Реализуем React Context с нуля через pub/sub паттерн.
// Это покажет, как Context работает "под капотом".
// --- Реализация createContext ---
function createContext(defaultValue) {
// Стек активных провайдеров для этого контекста
const providerStack = []
// Подписчики (потребители контекста)
const subscribers = new Set()
const context = {
// Provider "публикует" значение
Provider: {
mount(value) {
providerStack.push(value)
console.log(' [Provider] mounted, value:', JSON.stringify(value))
// Уведомляем всех потребителей о новом значении
subscribers.forEach(fn => fn(value))
},
unmount() {
providerStack.pop()
const prevValue = providerStack[providerStack.length - 1] || defaultValue
subscribers.forEach(fn => fn(prevValue))
console.log(' [Provider] unmounted, restored:', JSON.stringify(prevValue))
},
update(newValue) {
providerStack[providerStack.length - 1] = newValue
console.log(' [Provider] updated:', JSON.stringify(newValue))
// Все потребители получают новое значение
subscribers.forEach(fn => fn(newValue))
}
},
// useContext "подписывается" и читает текущее значение
useContext(onUpdate) {
const current = providerStack[providerStack.length - 1] || defaultValue
if (onUpdate) subscribers.add(onUpdate)
return {
value: current,
unsubscribe: () => subscribers.delete(onUpdate)
}
}
}
return context
}
// --- Тема: ThemeContext ---
const ThemeContext = createContext({ theme: 'light', toggle: null })
// Симуляция ThemeProvider
function mountThemeProvider() {
let theme = 'light'
const toggle = () => {
theme = theme === 'light' ? 'dark' : 'light'
ThemeContext.Provider.update({ theme, toggle })
}
ThemeContext.Provider.mount({ theme, toggle })
return { toggle }
}
// Симуляция компонента-потребителя
function Header() {
const onUpdate = (ctx) => {
console.log(' [Header] ре-рендер: тема = ' + ctx.theme)
}
const { value } = ThemeContext.useContext(onUpdate)
console.log(' [Header] первый рендер: тема = ' + value.theme)
return value
}
function Button() {
const onUpdate = (ctx) => {
console.log(' [Button] ре-рендер: тема = ' + ctx.theme)
}
const { value } = ThemeContext.useContext(onUpdate)
console.log(' [Button] первый рендер: тема = ' + value.theme)
return value
}
// --- Демонстрация ---
console.log('=== Монтирование ThemeProvider ===')
const { toggle } = mountThemeProvider()
console.log('\n=== Монтирование потребителей ===')
Header()
Button()
console.log('\n=== Переключение темы (оба потребителя ре-рендерятся) ===')
toggle() // Оба Header и Button получают уведомление!
console.log('\n=== Реальный React код (для справки): ===')
// const ThemeContext = React.createContext(null)
//
// function ThemeProvider({ children }) {
// const [theme, setTheme] = useState('light')
// const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
// return (
// <ThemeContext.Provider value={{ theme, toggle }}>
// {children}
// </ThemeContext.Provider>
// )
// }
//
// function Header() {
// const { theme } = useContext(ThemeContext)
// return <header className={theme}>...</header>
// }
console.log('// Используй createContext, Provider, useContext из React')Представьте дерево компонентов: App → Layout → Sidebar → UserAvatar. Если UserAvatar нужен текущий пользователь, придётся передавать user через Layout и Sidebar, хотя им самим он не нужен. Это называется prop drilling — "бурение пропсов".
// Проблема: Layout и Sidebar только "прокидывают" user, не используя его
function App() {
const user = { name: 'Алексей' }
return <Layout user={user} />
}
function Layout({ user }) {
return <Sidebar user={user} /> // Layout не использует user!
}
function Sidebar({ user }) {
return <UserAvatar user={user} /> // Sidebar тоже!
}
function UserAvatar({ user }) {
return <img alt={user.name} /> // Только здесь user нужен
}Context позволяет «прокинуть» данные через дерево без явной передачи через пропсы:
// 1. Создаём контекст с типом по умолчанию
const UserContext = React.createContext(null)
// 2. Оборачиваем дерево в Provider
function App() {
const user = { name: 'Алексей' }
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
)
}
// 3. Читаем из любого дочернего компонента
function UserAvatar() {
const user = useContext(UserContext)
return <img alt={user.name} />
}Context хорошо подходит для:
Context не подходит для часто меняющихся данных (каждый потребитель перерисуется), высокочастотных обновлений (счётчики, позиция мыши) и сложной логики с множеством действий (лучше Zustand/Redux).
Рекомендуется оборачивать логику провайдера в отдельный компонент и экспортировать кастомный хук:
// theme-context.jsx
const ThemeContext = createContext(null)
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
// Кастомный хук — защита от использования вне провайдера
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme должен использоваться внутри ThemeProvider')
return ctx
}Главный подводный камень: при изменении значения контекста все потребители (useContext) перерисовываются, даже если использованное ими поле не изменилось.
// Решение 1: разделить на несколько контекстов
const UserContext = createContext(null)
const ThemeContext = createContext(null)
// Решение 2: стабилизировать value через useMemo
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const value = useMemo(() => ({ user, theme }), [user, theme])
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}Можно вкладывать провайдеры — каждый компонент читает нужный:
function App() {
return (
<ThemeProvider>
<AuthProvider>
<LocaleProvider>
<Router />
</LocaleProvider>
</AuthProvider>
</ThemeProvider>
)
}| | Context | Zustand | Redux |
|---|---|---|---|
| Настройка | Минимальная | Минимальная | Много бойлерплейта |
| Производительность | Перерисовывает всех | Точечные обновления | Точечные обновления |
| DevTools | Нет | Есть (middleware) | Отличные |
| Подходит для | Статичные данные | Средние приложения | Большие команды |
Реализация Context API с нуля через паттерн pub/sub: createContext, Provider, useContext — без React
// Реализуем React Context с нуля через pub/sub паттерн.
// Это покажет, как Context работает "под капотом".
// --- Реализация createContext ---
function createContext(defaultValue) {
// Стек активных провайдеров для этого контекста
const providerStack = []
// Подписчики (потребители контекста)
const subscribers = new Set()
const context = {
// Provider "публикует" значение
Provider: {
mount(value) {
providerStack.push(value)
console.log(' [Provider] mounted, value:', JSON.stringify(value))
// Уведомляем всех потребителей о новом значении
subscribers.forEach(fn => fn(value))
},
unmount() {
providerStack.pop()
const prevValue = providerStack[providerStack.length - 1] || defaultValue
subscribers.forEach(fn => fn(prevValue))
console.log(' [Provider] unmounted, restored:', JSON.stringify(prevValue))
},
update(newValue) {
providerStack[providerStack.length - 1] = newValue
console.log(' [Provider] updated:', JSON.stringify(newValue))
// Все потребители получают новое значение
subscribers.forEach(fn => fn(newValue))
}
},
// useContext "подписывается" и читает текущее значение
useContext(onUpdate) {
const current = providerStack[providerStack.length - 1] || defaultValue
if (onUpdate) subscribers.add(onUpdate)
return {
value: current,
unsubscribe: () => subscribers.delete(onUpdate)
}
}
}
return context
}
// --- Тема: ThemeContext ---
const ThemeContext = createContext({ theme: 'light', toggle: null })
// Симуляция ThemeProvider
function mountThemeProvider() {
let theme = 'light'
const toggle = () => {
theme = theme === 'light' ? 'dark' : 'light'
ThemeContext.Provider.update({ theme, toggle })
}
ThemeContext.Provider.mount({ theme, toggle })
return { toggle }
}
// Симуляция компонента-потребителя
function Header() {
const onUpdate = (ctx) => {
console.log(' [Header] ре-рендер: тема = ' + ctx.theme)
}
const { value } = ThemeContext.useContext(onUpdate)
console.log(' [Header] первый рендер: тема = ' + value.theme)
return value
}
function Button() {
const onUpdate = (ctx) => {
console.log(' [Button] ре-рендер: тема = ' + ctx.theme)
}
const { value } = ThemeContext.useContext(onUpdate)
console.log(' [Button] первый рендер: тема = ' + value.theme)
return value
}
// --- Демонстрация ---
console.log('=== Монтирование ThemeProvider ===')
const { toggle } = mountThemeProvider()
console.log('\n=== Монтирование потребителей ===')
Header()
Button()
console.log('\n=== Переключение темы (оба потребителя ре-рендерятся) ===')
toggle() // Оба Header и Button получают уведомление!
console.log('\n=== Реальный React код (для справки): ===')
// const ThemeContext = React.createContext(null)
//
// function ThemeProvider({ children }) {
// const [theme, setTheme] = useState('light')
// const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light')
// return (
// <ThemeContext.Provider value={{ theme, toggle }}>
// {children}
// </ThemeContext.Provider>
// )
// }
//
// function Header() {
// const { theme } = useContext(ThemeContext)
// return <header className={theme}>...</header>
// }
console.log('// Используй createContext, Provider, useContext из React')Создай контекст темы с Provider и потребителями. Объяви ThemeContext через createContext. Компонент ThemeProvider хранит состояние темы и передаёт { theme, toggleTheme } через Provider. Компоненты Header и ThemedButton используют useContext для чтения темы. Компонент App оборачивает всё в ThemeProvider.
createContext(null) создаёт контекст. ThemeContext.Provider с атрибутом value передаёт данные вниз. useContext(ThemeContext) читает текущее значение. Дочерние компоненты не нужно явно пробрасывать через пропсы — они читают прямо из контекста.