← React/Context: глобальное состояние#267 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

Context: глобальное состояние

Проблема: prop drilling

Представьте дерево компонентов: 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 API

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 хорошо подходит для:

  • Темы оформления (светлая/тёмная)
  • Аутентификации (текущий пользователь, права)
  • Локализации (язык интерфейса)
  • Глобальных настроек приложения
  • 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 vs Zustand/Redux

    | | 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')

    Context: глобальное состояние

    Проблема: prop drilling

    Представьте дерево компонентов: 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 API

    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 хорошо подходит для:

  • Темы оформления (светлая/тёмная)
  • Аутентификации (текущий пользователь, права)
  • Локализации (язык интерфейса)
  • Глобальных настроек приложения
  • 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 vs Zustand/Redux

    | | 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) читает текущее значение. Дочерние компоненты не нужно явно пробрасывать через пропсы — они читают прямо из контекста.

    Загружаем среду выполнения...
    Загружаем AI-помощника...