Lekcja 16: Hook useContext (Context API)

Wzorzec "podnoszenia stanu" (lifting state up) jest świetny do współdzielenia stanu między kilkoma powiązanymi komponentami. Jednak gdy stan musi być dostępny dla wielu komponentów na różnych poziomach zagnieżdżenia, przekazywanie propsów przez wszystkie pośrednie komponenty (tzw. "prop drilling") staje się uciążliwe i zaciemnia kod. W takich sytuacjach z pomocą przychodzi Context API Reacta oraz Hook useContext.

Problem: Prop Drilling

Wyobraźmy sobie aplikację, gdzie informacja o zalogowanym użytkowniku lub preferowanym motywie (jasny/ciemny) jest potrzebna w wielu miejscach drzewa komponentów, często głęboko zagnieżdżonych. Przekazywanie tych danych jako propsów przez każdy poziom pośredni jest niewygodne i sprawia, że komponenty pośrednie muszą przyjmować i przekazywać propsy, których same nie używają.

// Przykład Prop Drilling
function App() {
  const theme = \'dark\";
  return <Toolbar theme={theme} />;
}

function Toolbar({ theme }) {
  // Toolbar nie używa theme, ale musi go przekazać dalej
  return (
    <div>
      <ThemedButton theme={theme} />
    </div>
  );
}

function ThemedButton({ theme }) {
  // Dopiero ten komponent faktycznie używa theme
  const style = { background: theme === \'dark\" ? \'#333\" : \'#FFF\", color: theme === \'dark\" ? \'#FFF\" : \'#333\" };
  return <button style={style}>Jestem stylizowany!</button>;
}

Przykład: Przekazywanie Motywu za pomocą Context

import React, { useState, useContext, createContext } from \'react\';

// 1. Stwórz Kontekst (można go umieścić w osobnym pliku)
const ThemeContext = createContext(\'light\'); // Wartość domyślna \'light\'

function App() {
  const [theme, setTheme] = useState(\'light\');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === \'light\" ? \'dark\" : \'light\'));
  };

  // Wartość przekazywana przez Providera może być obiektem
  const providerValue = {
      theme: theme,
      toggleTheme: toggleTheme
  };

  return (
    // 2. Dostarcz Wartość za pomocą Providera
    <ThemeContext.Provider value={providerValue}>
      <Layout />
    </ThemeContext.Provider>
  );
}

// Komponent pośredni - nie musi już wiedzieć o motywie!
function Layout() {
  return (
    <div>
      <Header />
      <Content />
    </div>
  );
}

function Header() {
    // 3. Odbierz Wartość za pomocą useContext
    const { theme, toggleTheme } = useContext(ThemeContext);
    const headerStyle = { padding: \'10px\', borderBottom: \'1px solid #ccc\', background: theme === \'dark\" ? \'#555\" : \'#EEE\" };

    return (
        <header style={headerStyle}>
            Aktualny motyw: {theme}
            <button onClick={toggleTheme} style={{ marginLeft: \'10px\" }}>
                Przełącz motyw
            </button>
        </header>
    );
}

function Content() {
  return (
    <main style={{ padding: \'10px\" }}>
      <p>Główna treść aplikacji...</p>
      <ThemedButton />
    </main>
  );
}

function ThemedButton() {
  // 3. Odbierz Wartość za pomocą useContext
  const { theme } = useContext(ThemeContext);
  
  const style = { 
      background: theme === \'dark\" ? \'#333\" : \'#FFF\", 
      color: theme === \'dark\" ? \'#FFF\" : \'#333\", 
      padding: \'8px\', 
      border: \'1px solid\'
  };

  return <button style={style}>Jestem stylizowany przez Context!</button>;
}

export default App;

Analiza przykładu:

Kiedy Używać Context API?

Context API jest przeznaczone do przekazywania danych, które można uznać za "globalne" dla danego drzewa komponentów React, takich jak:

Jest to alternatywa dla "prop drilling". Jednak nie jest to zamiennik dla wszystkich przypadków podnoszenia stanu. Jeśli stan jest potrzebny tylko przez kilka blisko powiązanych komponentów, podnoszenie stanu do najbliższego wspólnego przodka jest często prostszym i bardziej zrozumiałym rozwiązaniem.

Uwagi dotyczące Wydajności

Gdy wartość przekazywana przez Providera się zmienia, wszystkie komponenty, które używają useContext dla tego kontekstu, zostaną ponownie renderowane, nawet jeśli interesuje je tylko część przekazywanej wartości (np. gdy value jest obiektem i zmieniła się tylko jedna jego właściwość).

Aby zoptymalizować wydajność:

function App() {
  const [theme, setTheme] = useState(\'light\');

  // Memoizacja funkcji toggleTheme
  const toggleTheme = useCallback(() => {
    setTheme(prevTheme => (prevTheme === \'light\" ? \'dark\" : \'light\'));
  }, []); // Pusta tablica, bo funkcja nie zależy od niczego z zewnątrz

  // Memoizacja obiektu wartości providera
  const providerValue = useMemo(() => ({
      theme: theme,
      toggleTheme: toggleTheme
  }), [theme, toggleTheme]); // Zależności

  return (
    <ThemeContext.Provider value={providerValue}>
      <Layout />
    </ThemeContext.Provider>
  );
}

Ćwiczenie praktyczne

Pokaż rozwiązanie

Context API pozwala na przekazywanie danych w dół drzewa komponentów bez konieczności jawnego przekazywania propsów na każdym poziomie. Działa to na zasadzie "nadawcy" (Provider) i "odbiorcy" (Consumer lub Hook useContext).

Kroki użycia Context API:

  1. Stwórz Kontekst: Użyj funkcji React.createContext(defaultValue), aby stworzyć obiekt kontekstu. Argument defaultValue jest używany tylko wtedy, gdy komponent próbuje odczytać kontekst, ale nie ma nad sobą pasującego Providera.
  2. Dostarcz Wartość (Provider): Owiń część drzewa komponentów, która potrzebuje dostępu do danych, komponentem MyContext.Provider. Przekaż dane, które chcesz udostępnić, jako prop value do Providera.
  3. Odbierz Wartość (useContext): W dowolnym komponencie funkcyjnym *wewnątrz* drzewa objętego Providerem, użyj Hooka useContext(MyContext), aby uzyskać dostęp do aktualnej wartości kontekstu przekazanej przez najbliższego Providera powyżej.

Cel: Stworzyć prosty system zarządzania informacjami o zalogowanym użytkowniku za pomocą Context API.

Kroki:

  1. Stwórz kontekst (np. AuthContext) w osobnym pliku lub na początku pliku App.jsx. Wartość domyślna może być np. { user: null, login: () => {}, logout: () => {} }.
  2. W komponencie App użyj useState do przechowywania informacji o użytkowniku (np. currentUser, początkowo null).
  3. Zdefiniuj funkcje login(userData) i logout(), które będą aktualizować stan currentUser.
  4. Stwórz obiekt wartości dla Providera, zawierający currentUser, login i logout. Zmemoizuj go za pomocą useMemo.
  5. Owiń część aplikacji (np. jakiś komponent MainContent) komponentem AuthContext.Provider, przekazując zmemoizowaną wartość.
  6. Stwórz komponent UserProfile, który używa useContext(AuthContext) do odczytania currentUser. Jeśli użytkownik jest zalogowany, wyświetla jego imię, w przeciwnym razie komunikat "Nie jesteś zalogowany".
  7. Stwórz komponent LoginButton, który używa useContext(AuthContext). Jeśli użytkownik nie jest zalogowany, wyświetla przycisk "Zaloguj", który po kliknięciu wywołuje funkcję login z kontekstu (z przykładowymi danymi użytkownika).
  8. Stwórz komponent LogoutButton, który używa useContext(AuthContext). Jeśli użytkownik jest zalogowany, wyświetla przycisk "Wyloguj", który po kliknięciu wywołuje funkcję logout z kontekstu.
  9. Umieść komponenty UserProfile, LoginButton i LogoutButton wewnątrz drzewa objętego Providerem.

Rozwiązanie

// src/AuthContext.js (lub w App.jsx)
import { createContext } from \'react\';

const AuthContext = createContext({
  currentUser: null,
  login: () => { console.warn(\'login function not provided\'); },
  logout: () => { console.warn(\'logout function not provided\'); }
});

export default AuthContext;

// src/App.jsx
import React, { useState, useMemo, useCallback, useContext } from \'react\';
import AuthContext from \'./AuthContext\'; // Zaimportuj kontekst

// Komponenty podrzędne
function UserProfile() {
  const { currentUser } = useContext(AuthContext);
  return (
    <div>
      {currentUser ? (
        <p>Witaj, {currentUser.name}!</p>
      ) : (
        <p>Nie jesteś zalogowany.</p>
      )}
    </div>
  );
}

function LoginButton() {
  const { currentUser, login } = useContext(AuthContext);
  if (currentUser) return null; // Nie pokazuj, jeśli zalogowany
  return <button onClick={() => login({ name: \'Jan Kowalski\" })}>Zaloguj</button>;
}

function LogoutButton() {
  const { currentUser, logout } = useContext(AuthContext);
  if (!currentUser) return null; // Nie pokazuj, jeśli niezalogowany
  return <button onClick={logout}>Wyloguj</button>;
}

// Główny komponent aplikacji
function App() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((userData) => {
    console.log("Logowanie użytkownika:", userData);
    setCurrentUser(userData);
  }, []);

  const logout = useCallback(() => {
    console.log("Wylogowywanie użytkownika");
    setCurrentUser(null);
  }, []);

  const providerValue = useMemo(() => ({
    currentUser,
    login,
    logout
  }), [currentUser, login, logout]);

  return (
    <AuthContext.Provider value={providerValue}>
      <div style={{ padding: \'20px\" }}>
        <h1>Aplikacja z Kontekstem Autoryzacji</h1>
        <UserProfile />
        <LoginButton />
        <LogoutButton />
        {/* Inne komponenty aplikacji mogą być tutaj */}
      </div>
    </AuthContext.Provider>
  );
}

export default App;

Zadanie do samodzielnego wykonania

Rozszerz przykład z motywem (jasny/ciemny) z tej lekcji:

  1. Stwórz osobny plik dla kontekstu motywu (np. ThemeContext.js).
  2. Stwórz niestandardowy Hook useTheme, który po prostu wywołuje useContext(ThemeContext) i zwraca jego wartość. Użyj tego Hooka w komponentach Header i ThemedButton zamiast bezpośredniego wywołania useContext. (To częsta praktyka dla lepszej enkapsulacji).
  3. Dodaj więcej komponentów do aplikacji (np. Footer, Sidebar) i spraw, aby również reagowały na zmianę motywu, używając Hooka useTheme.

FAQ - Hook useContext (Context API)

Czy Context API zastępuje Reduxa lub inne biblioteki zarządzania stanem?

Niekoniecznie. Context API jest świetny do przekazywania danych w dół drzewa i unikania "prop drilling", zwłaszcza dla danych, które zmieniają się rzadko (jak motyw, język, informacje o użytkowniku). Dla bardziej złożonego, często aktualizowanego stanu globalnego, z zaawansowaną logiką (middleware, asynchroniczne akcje), biblioteki takie jak Redux, Zustand czy Jotai mogą oferować lepsze narzędzia, wydajność i strukturę.

Jaka jest różnica między `useContext` a `Context.Consumer`?

`Context.Consumer` to starszy sposób na odbieranie wartości z kontekstu, używany przed wprowadzeniem Hooków. Wymaga on użycia wzorca Render Props (``), co prowadzi do dodatkowego zagnieżdżenia. Hook `useContext` jest znacznie prostszy i czytelniejszy w komponentach funkcyjnych.

Czy mogę mieć wiele Providerów dla tego samego kontekstu?

Tak. Komponent używający `useContext` zawsze otrzyma wartość od **najbliższego** pasującego Providera znajdującego się powyżej niego w drzewie komponentów. Pozwala to na nadpisywanie wartości kontekstu w różnych częściach aplikacji.

Co się stanie, jeśli użyję `useContext` bez Providera powyżej?

W takim przypadku `useContext` zwróci wartość domyślną (), która została przekazana do `React.createContext(defaultValue)` podczas tworzenia kontekstu.

Czy wartość przekazywana przez `value` w Providerze może być dowolnego typu?

Tak, może to być dowolna wartość JavaScript: string, liczba, boolean, obiekt, tablica, funkcja itp. Często przekazuje się obiekt zawierający zarówno dane, jak i funkcje do ich modyfikacji.

Dlaczego muszę memoizować wartość Providera (`useMemo`, `useCallback`)?

Jeśli wartość przekazywana do `Providera` (zwłaszcza jeśli jest to obiekt lub tablica) jest tworzona na nowo przy każdym renderowaniu komponentu nadrzędnego, spowoduje to ponowne renderowanie *wszystkich* komponentów konsumujących ten kontekst, nawet jeśli faktyczna zawartość danych się nie zmieniła. Memoizacja zapobiega tworzeniu nowej referencji obiektu/funkcji, jeśli ich zależności się nie zmieniły, co optymalizuje wydajność.

Czy Context API nadaje się do bardzo często zmieniających się danych?

Należy być ostrożnym. Ponieważ zmiana wartości kontekstu powoduje ponowne renderowanie wszystkich komponentów, które go konsumują, może to prowadzić do problemów z wydajnością, jeśli dane zmieniają się bardzo często (np. pozycja myszy). W takich przypadkach inne rozwiązania (np. biblioteki zarządzania stanem z selektorami) mogą być bardziej odpowiednie.