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:
- Tworzymy
ThemeContext
za pomocącreateContext
. - Komponent
App
zarządza stanemtheme
i funkcjątoggleTheme
. App
owijaLayout
komponentemThemeContext.Provider
, przekazując obiekt{ theme, toggleTheme }
jakovalue
.- Komponenty
Header
iThemedButton
, które są głęboko zagnieżdżone, używająuseContext(ThemeContext)
, aby bezpośrednio uzyskać dostęp do wartościtheme
i funkcjitoggleTheme
, bez potrzeby przekazywania ich przezLayout
iContent
. - Gdy stan
theme
wApp
się zmienia (po kliknięciu przycisku wHeader
), Provider przekazuje nową wartość, a wszystkie komponenty używająceuseContext(ThemeContext)
są automatycznie ponownie renderowane z nową wartością.
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:
- Informacje o zalogowanym użytkowniku.
- Preferencje motywu (theme).
- Preferencje językowe.
- Dane konfiguracyjne.
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ść:
- Rozdzielaj konteksty: Jeśli masz różne, niezależne od siebie dane globalne, rozważ stworzenie dla nich osobnych kontekstów (np.
UserContext
,ThemeContext
). Zmiana w jednym kontekście nie spowoduje ponownego renderowania komponentów subskrybujących inny kontekst. - Memoizacja wartości Providera: Jeśli wartość przekazywana do
Providera
jest obiektem lub tablicą tworzoną wewnątrz komponentu renderującego Providera, może ona być tworzona na nowo przy każdym renderowaniu rodzica, nawet jeśli jej zawartość się nie zmieniła. Może to prowadzić do niepotrzebnych rerenderów konsumentów. Aby temu zapobiec, można zmemoizować wartość za pomocąuseMemo
(dla obiektów/tablic) iuseCallback
(dla funkcji).
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:
- Stwórz Kontekst: Użyj funkcji
React.createContext(defaultValue)
, aby stworzyć obiekt kontekstu. ArgumentdefaultValue
jest używany tylko wtedy, gdy komponent próbuje odczytać kontekst, ale nie ma nad sobą pasującego Providera. - 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 propvalue
do Providera. - Odbierz Wartość (
useContext
): W dowolnym komponencie funkcyjnym *wewnątrz* drzewa objętego Providerem, użyj HookauseContext(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:
- Stwórz kontekst (np.
AuthContext
) w osobnym pliku lub na początku plikuApp.jsx
. Wartość domyślna może być np.{ user: null, login: () => {}, logout: () => {} }
. - W komponencie
App
użyjuseState
do przechowywania informacji o użytkowniku (np.currentUser
, początkowonull
). - Zdefiniuj funkcje
login(userData)
ilogout()
, które będą aktualizować stancurrentUser
. - Stwórz obiekt wartości dla Providera, zawierający
currentUser
,login
ilogout
. Zmemoizuj go za pomocąuseMemo
. - Owiń część aplikacji (np. jakiś komponent
MainContent
) komponentemAuthContext.Provider
, przekazując zmemoizowaną wartość. - Stwórz komponent
UserProfile
, który używauseContext(AuthContext)
do odczytaniacurrentUser
. Jeśli użytkownik jest zalogowany, wyświetla jego imię, w przeciwnym razie komunikat "Nie jesteś zalogowany". - Stwórz komponent
LoginButton
, który używauseContext(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). - Stwórz komponent
LogoutButton
, który używauseContext(AuthContext)
. Jeśli użytkownik jest zalogowany, wyświetla przycisk "Wyloguj", który po kliknięciu wywołuje funkcjęlogout
z kontekstu. - Umieść komponenty
UserProfile
,LoginButton
iLogoutButton
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:
- Stwórz osobny plik dla kontekstu motywu (np.
ThemeContext.js
). - Stwórz niestandardowy Hook
useTheme
, który po prostu wywołujeuseContext(ThemeContext)
i zwraca jego wartość. Użyj tego Hooka w komponentachHeader
iThemedButton
zamiast bezpośredniego wywołaniauseContext
. (To częsta praktyka dla lepszej enkapsulacji). - Dodaj więcej komponentów do aplikacji (np.
Footer
,Sidebar
) i spraw, aby również reagowały na zmianę motywu, używając HookauseTheme
.
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.