Вивчай

Context API

React 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
Список елементів з callbackProps
Увага

Context не замінює props повністю. Якщо дані передаються на 1-2 рівні -- props простіший та зрозуміліший. Context -- для дійсно глобальних даних, що потрібні багатьом компонентам на різних рівнях.


Підсумок

  • Prop drilling -- передача props через багато рівнів, що не використовують ці дані
  • Context -- "телепортує" дані від Provider до будь-якого дочірнього компонента
  • Три кроки: createContextProvideruseContext
  • Custom hook (наприклад useTheme) -- стандартний патерн для Context
  • Зберігайте контексти в окремих файлах (src/contexts/)
  • Context для глобальних даних (тема, авторизація, мова), props -- для локальних

Що далі?

У наступному уроці навчимось додавати навігацію між сторінками за допомогою React Router -- routing для single-page додатків.

Інфо

Корисні посилання: