Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

W poprzednich wpisach nauczyliśmy się przekazywać dane przez props – od rodzica do dziecka. To działa świetnie dla prostych przypadków, ale co jeśli mamy głęboką hierarchię komponentów? Co jeśli komponent na 5 poziomie w dół potrzebuje danych z komponentu na samej górze?

Przekazywanie props przez każdy poziom (nawet gdy pośrednie komponenty ich nie potrzebują) to problem znany jako prop drilling. Jest męczący, podatny na błędy i trudny w utrzymaniu. Na szczęście React ma rozwiązanie: Context API.

W tym wpisie poznamy useContext Hook, nauczymy się tworzyć własne konteksty z pełnym TypeScript support, a na końcu odkryjemy moc custom hooks – sposobu na wyciąganie reużywalnej logiki z komponentów. To będzie przełomowy wpis – nauczysz się wzorców, które są fundamentem profesjonalnych aplikacji React!

Problem: Prop Drilling

Zacznijmy od zobaczenia problemu, który Context API rozwiązuje.

// App.tsx
function App() {
  const [user, setUser] = useState({ name: 'Paweł', theme: 'dark' });
  return <Dashboard user={user} />;
}

// Dashboard.tsx
function Dashboard({ user }) {
  return (
    <div>
      <Sidebar user={user} />
      <MainContent user={user} />
    </div>
  );
}

// Sidebar.tsx
function Sidebar({ user }) {
  return (
    <div>
      <UserWidget user={user} />
      <Navigation user={user} />
    </div>
  );
}

// UserWidget.tsx
function UserWidget({ user }) {
  return <div>Witaj, {user.name}!</div>;
}

Widzisz problem? user jest przekazywany przez Dashboard i Sidebar, mimo że te komponenty go nie używają! Są tylko "posłańcami" przekazującymi dane w dół.

Rozwiązanie: Context API

Context API pozwala "wynieść" dane na wyższy poziom i udostępnić je wszystkim komponentom w drzewie, bez przekazywania przez props.

Tworzenie Context

// ThemeContext.tsx
import { createContext, useState, useContext, type ReactNode } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');

  function toggleTheme() {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Użycie Context

// App.tsx
function App() {
  return (
    <ThemeProvider>
      <Dashboard />
    </ThemeProvider>
  );
}

// Header.tsx
function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header>
      <h1>Moja Aplikacja</h1>
      <button onClick={toggleTheme}>
        {theme === 'dark' ? '☀️ Jasny' : '🌙 Ciemny'}
      </button>
    </header>
  );
}

Żadnych props! Header bezpośrednio dostaje dostęp do theme przez useTheme().

Custom Hooks

Custom Hook to funkcja, która:

  1. Zaczyna się od use
  2. Może używać innych Hooków
  3. Zwraca dane i/lub funkcje

useLocalStorage

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue] as const;
}

Użycie:

function Counter() {
  const [count, setCount] = useLocalStorage('count', 0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

useToggle

function useToggle(initialValue: boolean = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue(prev => !prev);
  return [value, toggle] as const;
}

useFetch

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        if (!cancelled) setData(result);
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err.message : 'Error');
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchData();
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}
Praktyczny przykład: Auth Context

// AuthContext.tsx
import { createContext, useState, useContext, ReactNode } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  async function login(email: string, password: string) {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });
    const data = await response.json();
    setUser(data.user);
  }

  function logout() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
}

Użycie:

function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  // wyciągamy samą funkcję 'login' przez destrukturyzację
  const { login } = useAuth();

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    await login(email, password);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <button type="submit">Zaloguj</button>
    </form>
  );
}
Best Practices

1. Jeden Context = jedna odpowiedzialność

// ✅ Dobrze
const UserContext = createContext(/* user */);
const ThemeContext = createContext(/* theme */);
const CartContext = createContext(/* cart */);

2. Zawsze twórz custom hook

// ✅ Dobrze
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
}

3. TypeScript strict mode

// ✅ Dobrze
const MyContext = createContext<MyType | undefined>(undefined);
Kiedy używać Context vs Props?

Props: Dane potrzebne w bezpośrednim dziecku, 1-2 poziomy

Context: Dane potrzebne w wielu miejscach, prop drilling, globalne dane (theme, auth, cart)

Podsumowanie

Nauczyliśmy się:

  • Prop drilling – problem i jego skutki
  • Context APIcreateContext, Provider, useContext
  • TypeScript w Context – pełne typowanie
  • Custom Hooks – wyciąganie reużywalnej logiki
  • Praktyczne hookiuseLocalStorage, useToggle, useFetch
  • Auth Context – kompletny przykład
  • Best practices – profesjonalne wzorce

W kolejnym wpisie poznamy useReducer – sposób na zarządzanie złożonym stanem, idealny gdy useState nie wystarcza!

Zadanie dla Ciebie

  1. Stwórz TodoContext z zarządzaniem todos
  2. Napisz custom hook useTodoStats zwracający statystyki
  3. (Bonus) Hook usePersistedState łączący useState i localStorage