Przekazywanie propsów przez warstwy nieinteresujących się nimi komponentów to żmudna praca. Pewnego dnia wdrażasz nową funkcję, a następnego edytujesz sześć plików tylko po to, by zmienić nazwę jednego propa. To właśnie jest prop drilling w pigułce: rodzic posiada dane, głęboko zagnieżdżony komponent ich potrzebuje, a każdy komponent pomiędzy nimi staje się kurierem. Aplikacja nadal działa, ale baza kodu staje się krucha. Usuń warstwę pośrednią, a połowa drzewa się zawali. Zmień typ, a TypeScript zgłosi błędy w trzech różnych katalogach. React Context API istnieje po to, aby całkowicie wyeliminować tych pośredników.
Jak w rzeczywistości wygląda prop drilling
Wyobraź sobie standardową strukturę aplikacji. Masz komponent App, który pobiera dane aktualnego użytkownika. Wewnątrz App znajduje się Layout, wewnątrz Layout siedzi Sidebar, wewnątrz Sidebar zagnieżdżona jest Navigation, a na końcu wewnątrz Navigation znajdziesz UserAvatar, który faktycznie potrzebuje obiektu użytkownika.
Twój kod wygląda w ten sposób:
function App() {
const user = { name: 'Aarav', role: 'admin' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <Navigation user={user} />;
}
function Navigation({ user }) {
return <UserAvatar user={user} />;
}
Layout, Sidebar i Navigation nie robią z tym obiektem użytkownika nic poza przekazywaniem go dalej. Gromadzą propsy, których nie posiadają, ich interfejsy puchną, a testowanie ich wymaga mockowania danych, których nigdy nie dotykają. Prawdziwym problemem jest to, jak szybko się to rozprzestrzenia. Dodaj flagę isLoggedIn, ciąg locale lub wartość theme, a ten sam proces się powtórzy.
Jak Context API zmienia zasady gry
Myśl o Context API jak o routerze WiFi. Zamiast ciągnąć długie kable przez każdy pokój, aby dotrzeć do każdego urządzenia, router wysyła sygnał w powietrzu. Każde urządzenie w zasięgu może połączyć się bezpośrednio. W terminologii React router to Provider, sygnał to Twój stan lub dane, a urządzenie to dowolny zagnieżdżony komponent wywołujący useContext.
Konfiguracja składa się z trzech elementów:
React.createContext()buduje kanał danych.- Provider owija sekcję Twojego drzewa i przesyła wartość.
- Hook
useContextpozwala potomkom na otrzymanie tej wartości bez dotykania pośrednich propsów.
Nadal masz drzewo, ale gałęzie między korzeniem a liściem nie muszą już zawierać umowy z kurierem.
Budowanie kontekstu od zera
Zbudujmy konkretny przykład z ustawieniami motywu, ponieważ większość aplikacji w pewnym momencie potrzebuje trybu jasnego lub ciemnego.
Najpierw utwórz obiekt kontekstu. To jest rura:
import { createContext, useState, useMemo } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export default ThemeContext;
Następnie owiń swoją aplikację dostawcą (providerem). Zazwyczaj dzieje się to blisko korzenia:
import { ThemeProvider } from './ThemeContext';
function App() {
return (
<ThemeProvider>
<Layout />
</ThemeProvider>
);
}
Teraz każdy potomec może bezpośrednio odebrać sygnał. Oto przycisk przełączający, ukryty głęboko w interfejsie:
import { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(prev => prev === 'light' ? 'dark' : 'light')}
>
Current theme: {theme}
</button>
);
}
Zauważ, że Layout, Sidebar i Navigation nigdy nie widzą propa theme. Renderują się normalnie, a ThemeToggle pobiera to, czego potrzebuje, bezpośrednio z kontekstu. Okablowanie jest niewidoczne z zewnątrz, i o to właśnie chodzi.
Gdzie Context faktycznie pasuje
Context najlepiej sprawdza się w przypadku danych, które współdzielą wiele odległych komponentów, ale żaden pojedynczy rodzic nie posiada ich w sposób czysty. Dobrymi kandydatami są:
- Ustawienia motywu, takie jak tryb jasny lub ciemny, kolory akcentowe czy skalowanie czcionek.
- Stan uwierzytelnienia, taki jak obiekt aktualnego użytkownika, status logowania czy wygaśnięcie sesji.
- Ustawienia języka i lokalizacji do międzynarodowej wersji językowej.
- Dane koszyka zakupowego, które muszą być synchronizowane między odznaką w nagłówku, rozwijanym menu mini-koszyka a stroną kasy.
Powstrzymaj się od chęci wrzucenia każdego elementu stanu lokalnego do Contextu. Pole formularza dwa poziomy niżej nie potrzebuje globalnego nadawania. Zachowaj Context dla prawdziwych problemów przekrojowych (cross-cutting concerns), a resztę zostaw jako zwykłe propsy.
Pułapki wydajnościowe i jak ich unikać
Context nie jest darmowy. Gdy wartość kontekstu ulega aktualizacji, każdy komponent podpięty pod ten kontekst renderuje się ponownie, nawet jeśli fragment wartości, na którym mu zależy, nie uległ zmianie. Klasycznym błędem jest wrzucanie nowego literału obiektu do Providera przy każdym renderowaniu rodzica.
W naszym przykładzie z motywem, za każdym razem, gdy ThemeProvider renderuje się ponownie, ponieważ jego własny rodzic został zaktualizowany, wyrażenie { theme, setTheme } tworzy zupełnie nowy obiekt. React widzi nową referencję i każdy konsument zostaje zaktualizowany. Jeśli Twój motyw zmienia się rzadko, ale stan aplikacji zmienia się często, płacisz za rendery, których nie potrzebujesz.
Rozwiązanie jest dwuetapowe.
Rozdziel konteksty według częstotliwości aktualizacji. UserContext, który zmienia się raz podczas logowania, nie powinien współdzielić providera z NotificationContext, który aktualizuje się co kilka sekund. Trzymaj je oddzielnie, aby dane statyczne nie jechały tym samym pociągiem re-renderów co dane zmienne.
Owiń wartość w useMemo, gdy wartość jest obiektem lub tablicą. Daj Reactowi stabilną referencję:
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Now the object identity only shifts when theme actually changes. Descendant components that care about the context but are shielded by React.memo further down will skip the work.
Mistakes That Waste Your Time
The two errors that still show up in production code are easy to prevent.
First, forgetting to export the context itself. If you only export the Provider wrapper and keep the context object private, a developer writing a new feature cannot call useContext without refactoring your module. Export the context so consumers can import both the provider and the consumer hook cleanly.
Second, calling useContext outside the corresponding Provider. If ThemeToggle renders in a branch of the tree that is not wrapped in ThemeProvider, the hook returns the default value passed to createContext, or undefined if you passed none. This leads to silent failures like cannot read property of undefined. You can guard against this by assigning a sensible default or throwing a clear error early in the hook call.
Context vs Redux: Keep It Simple
You do not always need Redux. For medium-sized projects, Context paired with useState or useReducer covers the bulk of state sharing. Redux shines when you need time-travel debugging, complex middleware, or global transactions that must roll back in sequence. If your entire state story is a user object, a theme string, and a cart array, a store library adds boilerplate you will never leverage.
That said, Context is not a full state management system on its own. It does not give you a single global snapshot, and it does not batch updates across unrelated contexts. Use it as a replacement for prop drilling, not as an operating system for your entire data layer.
The Real Takeaway
Stop threading props through components that do not care about them. Create focused contexts for the data that actually spans your tree, wrap providers high enough to cover the consumers, and always stabilize the value object when you are passing collections or functions. Context API keeps yourReact code direct: props stay local, global data travels wirelessly, and your component boundaries stay clean.
