Pytania rekrutacyjne React Hooks

48 pytań — 20 odpowiedzi dostępnych od razu

Ten darmowy podgląd pytań rekrutacyjnych React Hooks jest dla osób, które chcą uporządkować pracę ze stanem i efektami w komponentach funkcyjnych. Prezentuje przykładowe odpowiedzi z zestawu 48 zagadnień.

W fiszkach pojawiają się useState, useEffect, useContext, useReducer oraz podstawy hooków. Materiał pomaga przygotować się do rozmowy o zależnościach efektów, ponownym renderowaniu i ponownym użyciu logiki.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 48 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 28 odpowiedzi

Kategoria 1: Podstawy React Hooks

Czym są React Hooks i dlaczego zostały wprowadzone?

Odpowiedź w 30 sekund: React Hooks to funkcje wprowadzone w React 16.8, które pozwalają używać stanu i innych funkcji React w komponentach funkcyjnych bez potrzeby pisania klas. Zostały stworzone, aby uprościć kod, ułatwić współdzielenie logiki między komponentami i rozwiązać problemy związane z this w JavaScript.

Odpowiedź w 2 minuty: React Hooks to specjalne funkcje, które "podpinają się" (hook into) do mechanizmów React, umożliwiając komponentom funkcyjnym korzystanie ze stanu, efektów ubocznych, kontekstu i innych funkcji frameworka. Przed wprowadzeniem Hooków, te możliwości były dostępne tylko w komponentach klasowych.

Główne powody wprowadzenia Hooków to: trudność w ponownym użyciu logiki stanowej między komponentami (wymagało to wzorców jak render props czy higher-order components), rosnąca złożoność komponentów klasowych z mieszaną logiką w metodach cyklu życia, oraz mylące zachowanie słowa kluczowego this w JavaScript. Hooki rozwiązują te problemy, pozwalając na ekstrakcję logiki do wielokrotnie używalnych funkcji (custom hooks), grupowanie powiązanej logiki w jednym miejscu, oraz eliminację problemów z this.

Hooki zachowują wszystkie koncepcje React, które już znamy (props, state, context, refs, lifecycle), ale zapewniają bardziej bezpośrednie API do ich wykorzystania. Dzięki temu kod staje się bardziej zwięzły, łatwiejszy do zrozumienia i testowania. React Hooks nie są rewolucją - to ewolucja, która pozwala pisać komponenty w bardziej funkcyjny i deklaratywny sposób.

Od wersji 16.8, React oficjalnie wspiera Hooki jako rekomendowany sposób pisania komponentów, przy czym komponenty klasowe nadal są w pełni wspierane dla zapewnienia kompatybilności wstecznej.

Przykład kodu:

// Komponent klasowy - stara metoda
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <p>Kliknięto {this.state.count} razy</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Kliknij
        </button>
      </div>
    );
  }
}

// Ten sam komponent z Hookami - nowoczesna metoda
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Kliknięto {count} razy</p>
      <button onClick={() => setCount(count + 1)}>
        Kliknij
      </button>
    </div>
  );
}

Diagram przepływu danych w Hookach:

graph TD
    A[Komponent funkcyjny] --> B[useState]
    A --> C[useEffect]
    A --> D[useContext]
    A --> E[Custom Hooks]
    B --> F[Stan lokalny]
    C --> G[Efekty uboczne]
    D --> H[Dane z kontekstu]
    E --> I[Logika wielokrotnego użytku]
    F --> J[Re-render]
    G --> J
    H --> J

Materiały:

Jakie problemy rozwiązują Hooki w porównaniu do komponentów klasowych?

Odpowiedź w 30 sekund: Hooki rozwiązują trzy główne problemy: trudność w wielokrotnym użyciu logiki stanowej (eliminując potrzebę HOC i render props), złożoność komponentów klasowych z rozproszoną logiką w metodach cyklu życia, oraz mylące zachowanie this w JavaScript. Pozwalają na grupowanie powiązanej logiki i jej łatwą ekstrakcję do wielokrotnie używalnych funkcji.

Odpowiedź w 2 minuty: Problem 1: Trudność w ponownym użyciu logiki stanowej W komponentach klasowych współdzielenie logiki wymagało zaawansowanych wzorców jak Higher-Order Components (HOC) lub render props, co prowadziło do "wrapper hell" - głęboko zagnieżdżonych komponentów trudnych do zrozumienia i debugowania. Hooki rozwiązują to poprzez custom hooks - zwykłe funkcje JavaScript, które mogą używać innych Hooków i są łatwe do współdzielenia między komponentami.

Problem 2: Złożoność komponentów klasowych W klasach powiązana logika była rozrzucona po różnych metodach cyklu życia (componentDidMount, componentDidUpdate, componentWillUnmount). Na przykład, subskrypcja mogła wymagać kodu w dwóch różnych metodach. Hooki, szczególnie useEffect, pozwalają grupować powiązaną logikę w jednym miejscu, co czyni kod bardziej spójnym i łatwiejszym w utrzymaniu.

Problem 3: Mylące this W komponentach klasowych this może być źródłem błędów, szczególnie dla początkujących. Wymaga bindowania metod w konstruktorze lub używania funkcji strzałkowych. Komponenty funkcyjne z Hookami eliminują ten problem całkowicie, używając zamknięć (closures) zamiast this.

Problem 4: Optymalizacja i minifikacja Komponenty klasowe nie minimalizują się tak dobrze jak funkcje, a nazwy metod klas mogą powodować problemy w produkcji. Komponenty funkcyjne są łatwiejsze do optymalizacji przez narzędzia do bundlowania.

Przykład kodu:

// PROBLEM: Logika rozproszona w komponentach klasowych
class UserProfile extends React.Component {
  componentDidMount() {
    // Subskrypcja statusu użytkownika
    this.subscription = userAPI.subscribe(
      this.props.userId,
      this.handleUserChange
    );

    // Pobieranie danych użytkownika
    this.fetchUserData(this.props.userId);

    // Nasłuchiwanie zmian rozmiaru okna
    window.addEventListener('resize', this.handleResize);
  }

  componentDidUpdate(prevProps) {
    // Ponowne pobieranie gdy zmieni się userId
    if (prevProps.userId !== this.props.userId) {
      this.fetchUserData(this.props.userId);

      // Zmiana subskrypcji
      this.subscription.unsubscribe();
      this.subscription = userAPI.subscribe(
        this.props.userId,
        this.handleUserChange
      );
    }
  }

  componentWillUnmount() {
    // Czyszczenie wszystkich subskrypcji
    this.subscription.unsubscribe();
    window.removeEventListener('resize', this.handleResize);
  }

  // Logika dla różnych funkcjonalności jest rozproszona!
}

// ROZWIĄZANIE: Hooki grupują powiązaną logikę
function UserProfile({ userId }) {
  // Logika związana z danymi użytkownika w jednym miejscu
  useEffect(() => {
    const subscription = userAPI.subscribe(userId, handleUserChange);
    fetchUserData(userId);

    return () => subscription.unsubscribe();
  }, [userId]); // Automatyczna aktualizacja przy zmianie userId

  // Logika związana z rozmiarem okna w osobnym useEffect
  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Wykonuje się raz

  // Kod jest bardziej czytelny i łatwiejszy w utrzymaniu
}

// BONUS: Ekstrakcja do custom hooka
function useUserSubscription(userId) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const subscription = userAPI.subscribe(userId, setUser);
    return () => subscription.unsubscribe();
  }, [userId]);

  return user;
}

// Teraz logikę można łatwo współdzielić
function UserProfile({ userId }) {
  const user = useUserSubscription(userId); // Wielokrotne użycie!
  // ...
}

Porównanie wzorców:

graph LR
    subgraph "Komponenty klasowe"
    A[HOC] --> B[Wrapper Hell]
    C[Render Props] --> B
    D[Mixins] --> E[Konflikty nazw]
    end

    subgraph "Hooki"
    F[Custom Hooks] --> G[Czyste funkcje]
    G --> H[Łatwe współdzielenie]
    G --> I[Brak zagnieżdżeń]
    end

Materiały:

Czy można używać Hooków w komponentach klasowych?

Odpowiedź w 30 sekund: Nie, Hooków nie można bezpośrednio używać wewnątrz komponentów klasowych. Hooki działają tylko w komponentach funkcyjnych. Jednak możliwe jest stopniowe migrowanie - komponenty klasowe mogą zawierać komponenty funkcyjne z Hookami jako dzieci, a logika z custom hooków może być dostępna poprzez komponenty opakowujące.

Odpowiedź w 2 minuty: React Hooks są zaprojektowane specjalnie dla komponentów funkcyjnych i nie działają wewnątrz komponentów klasowych. To jest celowa decyzja projektowa - Hooki opierają się na mechanizmach dostępnych tylko w funkcjach, takich jak closures i kolejność wywołań. Próba użycia Hooka w komponencie klasowym spowoduje błąd podczas wykonania.

Jednak React nie wymusza całkowitej migracji - istnieje kilka strategii współpracy między klasami a Hookami. Po pierwsze, można stopniowo migrować aplikację, pisząc nowe komponenty jako funkcyjne z Hookami, podczas gdy stare komponenty klasowe pozostają niezmienione. Komponenty klasowe mogą renderować komponenty funkcyjne jako dzieci, co pozwala na wprowadzanie Hooków bez przepisywania całej aplikacji.

Jeśli potrzebujesz logiki z custom hooka w komponencie klasowym, możesz utworzyć komponent funkcyjny wrapper, który używa hooka i przekazuje dane do komponentu klasowego przez props. Alternatywnie, można wyekstrahować logikę do zwykłej funkcji JavaScript (bez używania Hooków React), którą można wywołać zarówno w klasach, jak i w funkcjach.

React Team oficjalnie nie planuje usuwania wsparcia dla komponentów klasowych, więc nie ma presji na natychmiastową migrację całej aplikacji. Można przyjąć podejście inkrementalne, gdzie nowe funkcjonalności piszemy z Hookami, a stare komponenty klasowe przepisujemy tylko gdy jest to biznesowo uzasadnione lub konieczne z powodu zmian w kodzie.

Przykład kodu:

import { useState } from 'react';

// ❌ BŁĄD: Nie można używać Hooków w klasach
class MyClassComponent extends React.Component {
  render() {
    // To spowoduje błąd!
    const [count, setCount] = useState(0);

    return <div>{count}</div>;
  }
}

// ✅ POPRAWNIE: Hook w komponencie funkcyjnym
function MyFunctionalComponent() {
  const [count, setCount] = useState(0);

  return <div>{count}</div>;
}

// ✅ STRATEGIA 1: Komponenty klasowe mogą renderować funkcyjne z Hookami
class ParentClass extends React.Component {
  render() {
    return (
      <div>
        <h1>Komponent klasowy</h1>
        {/* Renderowanie komponentu funkcyjnego z Hookami */}
        <MyFunctionalComponent />
      </div>
    );
  }
}

// ✅ STRATEGIA 2: Wrapper dla custom hooka
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// Wrapper komponentu funkcyjnego
function WindowSizeWrapper({ children }) {
  const size = useWindowSize();
  return children(size); // Render prop pattern
}

// Użycie w komponencie klasowym
class Dashboard extends React.Component {
  render() {
    return (
      <WindowSizeWrapper>
        {(size) => (
          <div>
            Szerokość: {size.width}px, Wysokość: {size.height}px
          </div>
        )}
      </WindowSizeWrapper>
    );
  }
}

// ✅ STRATEGIA 3: HOC z Hookami dla komponentów klasowych
function withWindowSize(Component) {
  return function WrappedComponent(props) {
    const size = useWindowSize();
    return <Component {...props} windowSize={size} />;
  };
}

class MyClassWithSize extends React.Component {
  render() {
    const { windowSize } = this.props;
    return <div>Szerokość: {windowSize.width}px</div>;
  }
}

// Eksportuj opakowany komponent
export default withWindowSize(MyClassWithSize);

Diagram strategii migracji:

graph TD
    A[Aplikacja z komponentami klasowymi] --> B{Strategia migracji}
    B --> C[Stopniowa migracja]
    B --> D[Wrapper komponenty]
    B --> E[HOC z Hookami]

    C --> F[Nowe komponenty = funkcyjne]
    C --> G[Stare komponenty = klasowe]

    D --> H[Komponent funkcyjny z Hookiem]
    H --> I[Render prop do klasy]

    E --> J[HOC używa Hooków]
    J --> K[Przekazuje props do klasy]

    F --> L[Docelowo: tylko funkcyjne]
    G --> L

Materiały:

Jakie są dwa główne typy Hooków wbudowanych w React?

Odpowiedź w 30 sekund: Dwa najważniejsze wbudowane Hooki to useState (zarządzanie stanem lokalnym komponentu) i useEffect (wykonywanie efektów ubocznych jak pobieranie danych, subskrypcje czy manipulacja DOM). Te dwa Hooki pokrywają 90% potrzeb w typowych komponentach React i stanowią fundament dla innych Hooków.

Odpowiedź w 2 minuty: 1. useState - Hook do zarządzania stanem lokalnym useState pozwala dodać stan do komponentu funkcyjnego. Zwraca tablicę z dwoma elementami: aktualną wartością stanu oraz funkcją do jego aktualizacji. Jest odpowiednikiem this.state i this.setState z komponentów klasowych, ale prostszym i bardziej elastycznym. Można używać wielu wywołań useState w jednym komponencie, co pozwala na lepszą organizację stanu związanego z różnymi funkcjonalnościami.

2. useEffect - Hook do efektów ubocznych useEffect umożliwia wykonywanie efektów ubocznych w komponentach funkcyjnych. Zastępuje trzy metody cyklu życia z klas: componentDidMount, componentDidUpdate i componentWillUnmount. Pozwala na pobieranie danych, bezpośrednią manipulację DOM, ustawienie subskrypcji, timeoutów i wszystkich innych operacji, które "wychodzą na zewnątrz" React. Kluczowa funkcjonalność to opcjonalna funkcja czyszcząca zwracana z efektu oraz tablica zależności kontrolująca, kiedy efekt się wykonuje.

Oprócz tych dwóch podstawowych, React oferuje dodatkowe wbudowane Hooki: useContext (dostęp do React Context), useReducer (bardziej zaawansowane zarządzanie stanem), useCallback i useMemo (optymalizacja wydajności), useRef (dostęp do referencji DOM i wartości persystentnych), useLayoutEffect (synchroniczny efekt przed renderowaniem), oraz useImperativeHandle, useDebugValue do bardziej specjalistycznych zastosowań.

Te dwa główne Hooki (useState i useEffect) są fundamentem, na którym buduje się większość logiki komponentów. Zrozumienie ich działania jest kluczowe dla efektywnej pracy z React Hooks.

Przykład kodu:

import { useState, useEffect } from 'react';

// 1. useState - Zarządzanie stanem
function Counter() {
  // Deklaracja zmiennej stanu 'count' z wartością początkową 0
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  return (
    <div>
      <p>Licznik: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Zwiększ
      </button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>
        Zmniejsz (z funkcją aktualizującą)
      </button>

      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Wpisz imię"
      />
    </div>
  );
}

// 2. useEffect - Efekty uboczne
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Efekt wykonywany po każdym renderowaniu gdy zmieni się userId
  useEffect(() => {
    setLoading(true);

    // Pobieranie danych z API
    fetch(`/api/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });

    // Funkcja czyszcząca (opcjonalna)
    return () => {
      console.log('Czyszczenie przed następnym efektem');
    };
  }, [userId]); // Tablica zależności - efekt uruchomi się gdy zmieni się userId

  // Osobny efekt dla tytułu strony
  useEffect(() => {
    if (user) {
      document.title = `Profil: ${user.name}`;
    }

    // Czyszczenie przy odmontowaniu komponentu
    return () => {
      document.title = 'Moja aplikacja';
    };
  }, [user]); // Zależy tylko od user

  if (loading) return <div>Ładowanie...</div>;
  if (!user) return <div>Nie znaleziono użytkownika</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Przykład z subscrypcją i czyszczeniem
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // Subskrypcja do pokoju czatu
    const subscription = chatAPI.subscribe(roomId, (message) => {
      setMessages(prev => [...prev, message]);
    });

    // WAŻNE: Funkcja czyszcząca usuwa subskrypcję
    return () => {
      subscription.unsubscribe();
    };
  }, [roomId]); // Ponowna subskrypcja gdy zmieni się roomId

  return (
    <div>
      {messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
    </div>
  );
}

// Przykład kombinacji useState i useEffect
function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  useEffect(() => {
    // Nie szukaj jeśli query jest puste
    if (!query) {
      setResults([]);
      return;
    }

    setIsSearching(true);

    // Debouncing - czekaj 500ms przed wyszukiwaniem
    const timeoutId = setTimeout(() => {
      fetch(`/api/search?q=${query}`)
        .then(res => res.json())
        .then(data => {
          setResults(data);
          setIsSearching(false);
        });
    }, 500);

    // Czyszczenie timeouta jeśli query zmieni się przed upływem 500ms
    return () => clearTimeout(timeoutId);
  }, [query]); // Efekt uruchamia się gdy zmienia się query

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Szukaj użytkowników..."
      />
      {isSearching && <p>Wyszukiwanie...</p>}
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Cykl życia z useState i useEffect:

sequenceDiagram
    participant Component
    participant useState
    participant useEffect
    participant DOM

    Component->>useState: Inicjalizacja stanu
    useState-->>Component: [state, setState]
    Component->>DOM: Pierwszy render
    DOM->>useEffect: Wykonaj efekty

    Note over Component: Interakcja użytkownika
    Component->>useState: setState(newValue)
    useState->>Component: Aktualizacja stanu
    Component->>DOM: Re-render
    useEffect->>useEffect: Wykonaj cleanup poprzedniego efektu
    DOM->>useEffect: Wykonaj nowe efekty

    Note over Component: Odmontowanie
    useEffect->>useEffect: Wykonaj wszystkie cleanup functions

Porównanie z metodami klasowymi:

graph LR
    subgraph "Komponenty klasowe"
    A[constructor] --> A1[this.state]
    B[componentDidMount] --> B1[Efekty początkowe]
    C[componentDidUpdate] --> C1[Efekty po aktualizacji]
    D[componentWillUnmount] --> D1[Czyszczenie]
    end

    subgraph "Hooki"
    E[useState] --> A1
    F[useEffect z []] --> B1
    G[useEffect z deps] --> C1
    H[useEffect return] --> D1
    end

Materiały:

React Hooks - Kategoria 2: useState

Jak działa Hook useState i do czego służy?

Odpowiedź w 30 sekund: useState to podstawowy Hook w React, który pozwala na dodanie stanu do komponentów funkcyjnych. Zwraca tablicę z dwoma elementami: bieżącą wartością stanu oraz funkcją do jej aktualizacji. React zapamiętuje stan między kolejnymi renderowaniami komponentu.

Odpowiedź w 2 minuty: Hook useState umożliwia komponentom funkcyjnym przechowywanie i zarządzanie własnym stanem lokalnym, co wcześniej było możliwe tylko w komponentach klasowych. Wywołując useState z wartością początkową, otrzymujemy parę: aktualną wartość stanu oraz funkcję setter do jej modyfikacji.

React wykorzystuje kolejność wywołań Hooków do identyfikacji poszczególnych stanów w komponencie - dlatego Hooki muszą być zawsze wywoływane w tej samej kolejności. Kiedy wywołujemy funkcję aktualizującą stan, React planuje ponowne renderowanie komponentu z nową wartością. Co ważne, aktualizacje stanu są asynchroniczne i mogą być grupowane (batched) dla lepszej wydajności.

Stan utworzony przez useState jest izolowany dla każdej instancji komponentu - jeśli ten sam komponent jest renderowany wielokrotnie, każda instancja ma całkowicie niezależny stan. Hook useState może przechowywać dowolny typ danych: prymitywy, obiekty, tablice, a nawet funkcje (choć wymaga to specjalnej składni).

React gwarantuje, że referencja do funkcji setter pozostaje stała między renderowaniami, więc można bezpiecznie pomijać ją w dependency arrays w useEffect i innych Hookach. Jest to kluczowa optymalizacja umożliwiająca unikanie niepotrzebnych re-renderowań.

Przykład kodu:

import { useState } from 'react';

function Licznik() {
  // Inicjalizacja stanu z wartością 0
  const [liczba, setLiczba] = useState(0);
  const [tekst, setTekst] = useState('');

  return (
    <div>
      {/* Prosty stan liczbowy */}
      <p>Kliknięć: {liczba}</p>
      <button onClick={() => setLiczba(liczba + 1)}>
        Zwiększ
      </button>

      {/* Stan tekstowy */}
      <input
        value={tekst}
        onChange={(e) => setTekst(e.target.value)}
        placeholder="Wpisz tekst..."
      />
      <p>Wpisano: {tekst}</p>
    </div>
  );
}

// Przykład z wieloma stanami
function Formularz() {
  const [email, setEmail] = useState('');
  const [haslo, setHaslo] = useState('');
  const [zaakceptowanoRegulamin, setZaakceptowanoRegulamin] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ email, haslo, zaakceptowanoRegulamin });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={haslo}
        onChange={(e) => setHaslo(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={zaakceptowanoRegulamin}
          onChange={(e) => setZaakceptowanoRegulamin(e.target.checked)}
        />
        Akceptuję regulamin
      </label>
      <button type="submit">Wyślij</button>
    </form>
  );
}

Materiały

Jaka jest różnica między aktualizacją stanu za pomocą wartości a funkcji (functional update)?

Odpowiedź w 30 sekund: Aktualizacja bezpośrednią wartością (np. setCount(5)) ustawia nową wartość stanu, podczas gdy aktualizacja funkcyjna (np. setCount(prev => prev + 1)) otrzymuje poprzednią wartość jako argument. Aktualizacja funkcyjna jest niezbędna gdy nowy stan zależy od poprzedniego, szczególnie w callbackach asynchronicznych i event handlerach.

Odpowiedź w 2 minuty: Różnica między tymi podejściami staje się krytyczna w kontekście asynchroniczności React i batching'u aktualizacji stanu. Przy aktualizacji bezpośrednią wartością, używamy wartości stanu z aktualnego zakresu leksykalnego (closure), która może być przestarzała jeśli funkcja została utworzona wcześniej. Natomiast aktualizacja funkcyjna zawsze otrzymuje najbardziej aktualną wartość stanu od samego React.

W przypadku wielu szybkich aktualizacji stanu (np. wielokrotne kliknięcia przycisku), React może grupować je w jednym cyklu renderowania dla optymalizacji. Przy użyciu bezpośredniej wartości, wszystkie aktualizacje bazują na tej samej poprzedniej wartości, co prowadzi do zgubienia niektórych zmian. Aktualizacja funkcyjna gwarantuje, że każda zmiana jest aplikowana sekwencyjnie na wyniku poprzedniej.

To szczególnie ważne w event handlerach, setTimeout, setInterval, oraz w funkcjach przekazywanych do komponentów potomnych. W tych kontekstach często operujemy na "zatrzaśniętej" (captured) wartości stanu z momentu utworzenia funkcji. Aktualizacja funkcyjna rozwiązuje ten problem domknięć (closure problem), ponieważ React przekazuje aktualną wartość jako argument.

Dodatkową zaletą aktualizacji funkcyjnej jest lepsza czytelność kodu - od razu widać, że nowy stan zależy od poprzedniego. Funkcja aktualizująca musi być pure function - nie powinna mieć efektów ubocznych, a dla tej samej wartości wejściowej zawsze powinna zwracać ten sam wynik.

Przykład kodu:

import { useState } from 'react';

function ProblemZClosure() {
  const [licznik, setLicznik] = useState(0);

  // ❌ BŁĄD: Aktualizacja bezpośrednią wartością
  const handleKliknieciaBledne = () => {
    // Wszystkie trzy wywołania używają tej samej wartości licznik (0)
    // Wynik: licznik = 1, zamiast 3
    setLicznik(licznik + 1);
    setLicznik(licznik + 1);
    setLicznik(licznik + 1);
  };

  // ✅ POPRAWNE: Aktualizacja funkcyjna
  const handleKliknieciaPoprawne = () => {
    // Każde wywołanie otrzymuje poprzedni wynik
    // Wynik: licznik = 3
    setLicznik(prev => prev + 1);
    setLicznik(prev => prev + 1);
    setLicznik(prev => prev + 1);
  };

  return (
    <div>
      <p>Licznik: {licznik}</p>
      <button onClick={handleKliknieciaBledne}>
        Błędne (+3 da tylko +1)
      </button>
      <button onClick={handleKliknieciaPoprawne}>
        Poprawne (+3)
      </button>
    </div>
  );
}

// Problem z setTimeout i domknięciami
function StoperProblemy() {
  const [sekundy, setSekundy] = useState(0);

  // ❌ BŁĄD: Wartość 'sekundy' jest zatrzaśnięta w closure
  const startBledny = () => {
    setInterval(() => {
      setSekundy(sekundy + 1); // Zawsze używa początkowej wartości 0
    }, 1000);
  };

  // ✅ POPRAWNE: Funkcja zawsze dostaje aktualną wartość
  const startPoprawny = () => {
    setInterval(() => {
      setSekundy(prev => prev + 1); // Zawsze aktualna wartość
    }, 1000);
  };

  return (
    <div>
      <p>Upłynęło sekund: {sekundy}</p>
      <button onClick={startPoprawny}>Start</button>
    </div>
  );
}

// Złożony przykład z warunkami
function LicznikZLimitem() {
  const [licznik, setLicznik] = useState(0);
  const MAKSIMUM = 10;

  const zwiekszBezpiecznie = () => {
    setLicznik(prev => {
      // Możemy użyć logiki warunkowej
      if (prev >= MAKSIMUM) {
        return prev; // Nie zmieniaj jeśli osiągnięto limit
      }
      return prev + 1;
    });
  };

  const zmienWiele = (delta) => {
    setLicznik(prev => {
      const nowaWartosc = prev + delta;
      // Ograniczamy wartość do przedziału 0-MAKSIMUM
      return Math.max(0, Math.min(nowaWartosc, MAKSIMUM));
    });
  };

  return (
    <div>
      <p>Licznik: {licznik} / {MAKSIMUM}</p>
      <button onClick={zwiekszBezpiecznie}>+1</button>
      <button onClick={() => zmienWiele(5)}>+5</button>
      <button onClick={() => zmienWiele(-3)}>-3</button>
    </div>
  );
}

Materiały

Jak zarządzać stanem obiektowym za pomocą useState?

Odpowiedź w 30 sekund: Stan obiektowy wymaga tworzenia nowego obiektu przy każdej aktualizacji, ponieważ React porównuje referencje. Należy używać spread operatora (...) do kopiowania istniejących właściwości i nadpisywania tylko tych, które się zmieniają. Dla złożonych obiektów można rozważyć useReducer lub podział na mniejsze stany.

Odpowiedź w 2 minuty: Zarządzanie stanem obiektowym w useState wymaga przestrzegania zasady immutability - nigdy nie modyfikujemy obiektu bezpośrednio, zawsze tworzymy nową kopię. React wykrywa zmiany przez porównanie referencji (shallow comparison), więc mutowanie obiektu nie spowoduje re-renderowania, nawet jeśli wartości się zmieniły.

Podstawową techniką jest użycie spread operatora do skopiowania wszystkich właściwości obiektu, a następnie nadpisanie konkretnych pól. Jest to tzw. shallow copy - kopiuje tylko pierwszy poziom właściwości. Dla zagnieżdżonych obiektów trzeba stosować spread operator rekurencyjnie na każdym poziomie, co może być uciążliwe.

Ważną decyzją architektoniczną jest czy trzymać dane w jednym dużym obiekcie, czy w wielu oddzielnych stanach. Pojedynczy obiekt jest wygodny gdy dane są ściśle powiązane (np. formularz), ale utrudnia optymalizację - każda zmiana powoduje re-renderowanie komponentu, nawet jeśli zmieniło się tylko jedno pole. Rozdzielenie na osobne useState może poprawić wydajność poprzez bardziej granularne aktualizacje.

Dla bardzo złożonych struktur danych z wieloma zagnieżdżeniami warto rozważyć biblioteki jak Immer, które oferują API oparte na mutacjach, ale pod spodem zapewniają immutability. Alternatywnie, useReducer może być lepszym wyborem dla skomplikowanej logiki aktualizacji stanu obiektowego, szczególnie gdy wiele różnych akcji modyfikuje ten sam stan.

Przykład kodu:

import { useState } from 'react';

function FormularzUzytkownika() {
  // Pojedynczy obiekt stanu dla formularza
  const [uzytkownik, setUzytkownik] = useState({
    imie: '',
    nazwisko: '',
    email: '',
    wiek: 0
  });

  // ❌ BŁĄD: Bezpośrednia mutacja
  const handleZmianaZle = (pole, wartosc) => {
    uzytkownik[pole] = wartosc; // Nie zadziała!
    setUzytkownik(uzytkownik); // React nie wykryje zmiany
  };

  // ✅ POPRAWNE: Tworzenie nowego obiektu
  const handleZmiana = (pole, wartosc) => {
    setUzytkownik(prev => ({
      ...prev,           // Kopiuj wszystkie istniejące pola
      [pole]: wartosc    // Nadpisz tylko jedno pole
    }));
  };

  // Wersja z destrukturyzacją dla czytelności
  const handleZmianaV2 = (e) => {
    const { name, value } = e.target;
    setUzytkownik(prev => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <input
        name="imie"
        value={uzytkownik.imie}
        onChange={handleZmianaV2}
        placeholder="Imię"
      />
      <input
        name="nazwisko"
        value={uzytkownik.nazwisko}
        onChange={handleZmianaV2}
        placeholder="Nazwisko"
      />
      <input
        name="email"
        type="email"
        value={uzytkownik.email}
        onChange={handleZmianaV2}
        placeholder="Email"
      />
      <input
        name="wiek"
        type="number"
        value={uzytkownik.wiek}
        onChange={handleZmianaV2}
        placeholder="Wiek"
      />
      <pre>{JSON.stringify(uzytkownik, null, 2)}</pre>
    </form>
  );
}

// Zagnieżdżone obiekty - wymaga głębszego kopiowania
function ProfilUzytkownika() {
  const [profil, setProfil] = useState({
    dane: {
      imie: 'Jan',
      nazwisko: 'Kowalski'
    },
    adres: {
      ulica: 'Główna 1',
      miasto: 'Warszawa',
      kod: '00-001'
    },
    ustawienia: {
      powiadomienia: true,
      newsletter: false
    }
  });

  // Aktualizacja zagnieżdżonej właściwości
  const aktualizujMiasto = (noweMiasto) => {
    setProfil(prev => ({
      ...prev,                    // Kopiuj główny obiekt
      adres: {
        ...prev.adres,            // Kopiuj obiekt adres
        miasto: noweMiasto        // Zmień tylko miasto
      }
    }));
  };

  // Przełączanie boolean w zagnieżdżonym obiekcie
  const przelaczPowiadomienia = () => {
    setProfil(prev => ({
      ...prev,
      ustawienia: {
        ...prev.ustawienia,
        powiadomienia: !prev.ustawienia.powiadomienia
      }
    }));
  };

  return (
    <div>
      <p>Miasto: {profil.adres.miasto}</p>
      <button onClick={() => aktualizujMiasto('Kraków')}>
        Zmień na Kraków
      </button>
      <label>
        <input
          type="checkbox"
          checked={profil.ustawienia.powiadomienia}
          onChange={przelaczPowiadomienia}
        />
        Powiadomienia
      </label>
    </div>
  );
}

// Alternatywa: Rozdzielone stany (lepsza wydajność)
function FormularzRozdzielony() {
  // Zamiast jednego obiektu, osobne stany
  const [imie, setImie] = useState('');
  const [nazwisko, setNazwisko] = useState('');
  const [email, setEmail] = useState('');
  const [wiek, setWiek] = useState(0);

  // Każde pole może być aktualizowane niezależnie
  // Re-renderowanie tylko gdy konkretne pole się zmienia

  return (
    <form>
      <input
        value={imie}
        onChange={(e) => setImie(e.target.value)}
        placeholder="Imię"
      />
      <input
        value={nazwisko}
        onChange={(e) => setNazwisko(e.target.value)}
        placeholder="Nazwisko"
      />
      {/* ... pozostałe pola */}
    </form>
  );
}

// Helper funkcja dla głębokiej aktualizacji
function ZaawansowanaAktualizacja() {
  const [dane, setDane] = useState({
    uzytkownik: { imie: 'Jan', adres: { miasto: 'Warszawa' } }
  });

  // Pomocnicza funkcja do aktualizacji głęboko zagnieżdżonych wartości
  const aktualizujGleboko = (sciezka, wartosc) => {
    setDane(prev => {
      const nowy = { ...prev };
      const klucze = sciezka.split('.');
      let aktualny = nowy;

      // Nawiguj do przedostatniego poziomu
      for (let i = 0; i < klucze.length - 1; i++) {
        aktualny[klucze[i]] = { ...aktualny[klucze[i]] };
        aktualny = aktualny[klucze[i]];
      }

      // Ustaw końcową wartość
      aktualny[klucze[klucze.length - 1]] = wartosc;
      return nowy;
    });
  };

  return (
    <button onClick={() => aktualizujGleboko('uzytkownik.adres.miasto', 'Gdańsk')}>
      Zmień miasto
    </button>
  );
}

Materiały

Dlaczego useState zwraca tablicę, a nie obiekt?

Odpowiedź w 30 sekund: useState zwraca tablicę aby umożliwić łatwą destrukturyzację z własnymi nazwami zmiennych. Gdyby zwracał obiekt, musielibyśmy używać tych samych kluczy lub stosować aliasy. Tablica pozwala na zwięzłą składnię [nazwa, setNazwa] i używanie tego samego Hooka wielokrotnie z różnymi nazwami.

Odpowiedź w 2 minuty: Decyzja o zwracaniu tablicy zamiast obiektu jest świadomym wyborem designu API mającym na celu maksymalizację wygody i elastyczności użycia. Kluczową zaletą jest możliwość nadawania dowolnych nazw podczas destrukturyzacji bez konieczności używania aliasów czy zachowywania określonych kluczy.

Przy destrukturyzacji tablicy nazwy zmiennych są całkowicie dowolne i zależą tylko od pozycji w tablicy. Dzięki temu możemy w jednym komponencie używać useState wielokrotnie, za każdym razem nadając semantyczne nazwy odpowiadające naszej logice biznesowej. Gdyby useState zwracał obiekt z konkretnymi kluczami jak {value, setValue}, musielibyśmy albo używać tych samych nazw wszędzie (niepraktyczne), albo stosować składnię aliasów {value: count, setValue: setCount}, która jest znacznie bardziej rozwlekła.

Warto zauważyć, że kolejność elementów w tablicy jest zawsze taka sama i dobrze udokumentowana: pierwszy element to wartość stanu, drugi to funkcja aktualizująca. Ta przewidywalność czyni API intuicyjnym i łatwym do zapamiętania. Konwencja nazewnicza setState* jest powszechnie przyjęta w społeczności React, co zwiększa czytelność kodu między różnymi projektami.

Dodatkowo, tablica z dokładnie dwoma elementami jest bardziej lightweight niż obiekt - nie ma narzutu związanego z kluczami stringowymi. Z perspektywy TypeScript, typy dla tablic z ustaloną liczbą elementów (tuples) są również bardzo dobrze wspierane i oferują silną inferencję typów dla każdego elementu osobno.

Przykład kodu:

import { useState } from 'react';

function PorowanieSkładni() {
  // ✅ AKTUALNE: useState zwraca tablicę
  const [licznik, setLicznik] = useState(0);
  const [tekst, setTekst] = useState('');
  const [zaznaczony, setZaznaczony] = useState(false);

  // Dowolne nazwy, zwięzła składnia
  // Można używać wielokrotnie bez konfliktów

  // ❌ GDYBY zwracał obiekt z ustalonymi kluczami:
  // const { value, setValue } = useState(0);
  // const { value, setValue } = useState(''); // Konflikt nazw!

  // Musielibyśmy stosować aliasy (rozwlekłe):
  // const { value: licznik, setValue: setLicznik } = useState(0);
  // const { value: tekst, setValue: setTekst } = useState('');
  // const { value: zaznaczony, setValue: setZaznaczony } = useState(false);

  return (
    <div>
      <p>Licznik: {licznik}</p>
      <button onClick={() => setLicznik(licznik + 1)}>+</button>

      <input value={tekst} onChange={(e) => setTekst(e.target.value)} />

      <label>
        <input
          type="checkbox"
          checked={zaznaczony}
          onChange={(e) => setZaznaczony(e.target.checked)}
        />
        Zaznacz
      </label>
    </div>
  );
}

// Przykład z wieloma stanami o semantycznych nazwach
function FormularzRejestracji() {
  // Każdy stan ma opisową nazwę dopasowaną do kontekstu
  const [email, setEmail] = useState('');
  const [haslo, setHaslo] = useState('');
  const [potwierdzHasla, setPotwierdzHasla] = useState('');
  const [imie, setImie] = useState('');
  const [nazwisko, setNazwisko] = useState('');
  const [dataUrodzenia, setDataUrodzenia] = useState('');
  const [akceptujeRegulamin, setAkceptujeRegulamin] = useState(false);
  const [newsletterZgoda, setNewsletterZgoda] = useState(false);
  const [krajZamieszkania, setKrajZamieszkania] = useState('PL');

  // Gdyby useState zwracał obiekt, każda z powyższych linii
  // byłaby znacznie dłuższa i mniej czytelna

  return (
    <form>
      {/* Formularz używający wszystkich stanów */}
    </form>
  );
}

// Destrukturyzacja tablicy vs obiektu - różnice
function PrzykladDestrukturyzacji() {
  // Tablica - nazwy dowolne, kolejność ma znaczenie
  const [pierwszy, drugi] = useState(0);
  const [a, b] = useState(10);
  const [wartosc, aktualizuj] = useState('');

  // Wszystkie powyższe są poprawne, nazwy są elastyczne

  // Gdyby był obiekt - nazwy ustalone, kolejność nie ma znaczenia
  // const { value: pierwszy, setValue: drugi } = useState(0);
  // const { setValue: b, value: a } = useState(10); // Kolejność nie ma znaczenia

  // Dodatkowy przykład: możemy zignorować funkcję setter jeśli jej nie używamy
  const [tylko_odczyt] = useState('stała wartość');
  // Z obiektem: const { value: tylko_odczyt } = useState('stała wartość');

  // Możemy też zignorować wartość i wziąć tylko setter (rzadkie)
  const [, setTylkoSetter] = useState(0);
  // Z obiektem: const { setValue: setTylkoSetter } = useState(0);

  return null;
}

// Wzorzec z konwencją nazewniczą
function DobryWzorzecNazewnictwa() {
  // Konwencja: [rzeczownik, setRzeczownik]
  const [uzytkownik, setUzytkownik] = useState(null);
  const [produkty, setProdukty] = useState([]);
  const [ladowanie, setLadowanie] = useState(false);
  const [blad, setBlad] = useState(null);

  // Dla boolean często używa się is/has prefix
  const [jestZalogowany, setJestZalogowany] = useState(false);
  const [maPowiadomienia, setMaPowiadomienia] = useState(false);

  // Ta konwencja jest powszechna w społeczności React
  // i zwiększa czytelność kodu

  return null;
}

// TypeScript - typy dla tuple vs obiekt
function TypeScriptPrzyklad() {
  // TypeScript automatycznie rozpoznaje tuple type
  const [licznik, setLicznik] = useState<number>(0);
  // Typ: [number, Dispatch<SetStateAction<number>>]

  const [dane, setDane] = useState<{ id: number; nazwa: string } | null>(null);
  // Typ: [{ id: number; nazwa: string } | null, Dispatch<SetStateAction<...>>]

  // Silna inferencja typów dla każdego elementu osobno

  return null;
}

Materiały

Jak zainicjować stan za pomocą kosztownej funkcji (lazy initialization)?

Odpowiedź w 30 sekund: Lazy initialization polega na przekazaniu funkcji zamiast wartości do useState. Funkcja jest wywoływana tylko przy pierwszym renderowaniu, co optymalizuje wydajność gdy inicjalizacja jest kosztowna (np. czytanie z localStorage, złożone obliczenia). Składnia: useState(() => kosztownaOperacja()) zamiast useState(kosztownaOperacja()).

Odpowiedź w 2 minuty: Lazy initialization jest techniką optymalizacyjną która zapobiega niepotrzebnemu wykonywaniu kosztownych operacji przy każdym re-renderowaniu komponentu. Kiedy przekazujemy wartość bezpośrednio do useState, wyrażenie jest ewaluowane przy każdym renderowaniu, nawet jeśli wynik jest używany tylko za pierwszym razem. Przekazując funkcję, React wywołuje ją wyłącznie podczas inicjalizacji stanu.

Jest to szczególnie ważne dla operacji takich jak: odczyt z localStorage lub sessionStorage, parsowanie dużych struktur JSON, złożone obliczenia matematyczne, generowanie unikalnych ID, czy deserializacja danych. Bez lazy initialization te operacje byłyby wykonywane przy każdym renderze, marnując zasoby, mimo że ich wynik jest pomijany (useState używa wartości początkowej tylko raz).

Funkcja inicjalizująca musi być pure function bez parametrów - może odczytywać zewnętrzne zmienne przez closure, ale nie powinna mieć efektów ubocznych. React gwarantuje, że zostanie wywołana dokładnie raz na cykl życia instancji komponentu. Jeśli komponent zostanie odmontowany i zamontowany ponownie, funkcja zostanie wywołana ponownie dla nowej instancji.

Warto zauważyć, że lazy initialization to optymalizacja, nie zawsze konieczna. Dla prostych wartości jak liczby, stringi czy małe obiekty, bezpośrednie przekazanie wartości jest całkowicie akceptowalne. Optymalizacja powinna być stosowana świadomie, gdy mierzymy faktyczny problem z wydajnością, a nie prematurely.

Przykład kodu:

import { useState } from 'react';

// ❌ NIEOPTYMALNE: Funkcja wykonywana przy każdym renderze
function KomponentNieOptymalizowany() {
  // localStorage.getItem wykonywane za każdym razem!
  const [uzytkownik, setUzytkownik] = useState(
    JSON.parse(localStorage.getItem('uzytkownik') || 'null')
  );

  // Ta funkcja jest wywoływana przy każdym renderze,
  // mimo że wynik jest używany tylko za pierwszym razem

  return <div>{uzytkownik?.imie}</div>;
}

// ✅ OPTYMALNE: Lazy initialization
function KomponentOptymalizowany() {
  // Funkcja strzałkowa wykonywana TYLKO przy pierwszym renderze
  const [uzytkownik, setUzytkownik] = useState(() => {
    const zapisany = localStorage.getItem('uzytkownik');
    return zapisany ? JSON.parse(zapisany) : null;
  });

  // Możemy też dodać logowanie żeby zobaczyć różnicę
  // console.log('Inicjalizacja!') - zobaczymy tylko raz

  return <div>{uzytkownik?.imie}</div>;
}

// Przykład ze złożonymi obliczeniami
function GeneratorLiczb() {
  // ❌ ŹLE: Obliczenia przy każdym renderze
  const [liczby, setLiczby] = useState(
    Array.from({ length: 10000 }, (_, i) => Math.random() * i)
  );

  // ✅ DOBRZE: Obliczenia tylko raz
  const [liczbyOpt, setLiczbyOpt] = useState(() =>
    Array.from({ length: 10000 }, (_, i) => Math.random() * i)
  );

  return null;
}

// Praktyczny przykład z localStorage i synchronizacją
function UstawieniaAplikacji() {
  const [ustawienia, setUstawienia] = useState(() => {
    try {
      // Próba odczytu z localStorage
      const zapisaneUstawienia = localStorage.getItem('appSettings');

      if (zapisaneUstawienia) {
        return JSON.parse(zapisaneUstawienia);
      }

      // Domyślne ustawienia jeśli nic nie zapisano
      return {
        motyw: 'jasny',
        jezyk: 'pl',
        powiadomienia: true,
        rozmiarCzcionki: 16
      };
    } catch (error) {
      // Obsługa błędów (np. nieprawidłowy JSON)
      console.error('Błąd wczytywania ustawień:', error);
      return {
        motyw: 'jasny',
        jezyk: 'pl',
        powiadomienia: true,
        rozmiarCzcionki: 16
      };
    }
  });

  // Zapisz do localStorage przy każdej zmianie
  const aktualizujUstawienia = (noweUstawienia) => {
    setUstawienia(noweUstawienia);
    localStorage.setItem('appSettings', JSON.stringify(noweUstawienia));
  };

  return (
    <div>
      <select
        value={ustawienia.motyw}
        onChange={(e) => aktualizujUstawienia({
          ...ustawienia,
          motyw: e.target.value
        })}
      >
        <option value="jasny">Jasny</option>
        <option value="ciemny">Ciemny</option>
      </select>
    </div>
  );
}

// Przykład z generowaniem unikalnego ID
function FormularzZID() {
  // Generuj unikalne ID tylko raz przy tworzeniu komponentu
  const [formId] = useState(() => {
    return `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  });

  // ID pozostaje stałe przez cały cykl życia komponentu

  return (
    <form id={formId}>
      <label htmlFor={`${formId}-email`}>Email:</label>
      <input id={`${formId}-email`} type="email" />
    </form>
  );
}

// Przykład z kosztownymi obliczeniami i warunkami
function AnalizaDanych({ poczatkoweDane }) {
  const [przetworzoneDane, setPrzetworzoneDane] = useState(() => {
    console.log('Rozpoczynam kosztowne przetwarzanie...');

    if (!poczatkoweDane || poczatkoweDane.length === 0) {
      return [];
    }

    // Symulacja kosztownej operacji
    return poczatkoweDane
      .map(item => ({
        ...item,
        // Złożone transformacje
        processed: item.value * 2,
        calculated: Math.sqrt(item.value),
        formatted: item.value.toFixed(2)
      }))
      .filter(item => item.processed > 10)
      .sort((a, b) => b.calculated - a.calculated);
  });

  return (
    <ul>
      {przetworzoneDane.map((item, idx) => (
        <li key={idx}>{item.formatted}</li>
      ))}
    </ul>
  );
}

// Custom Hook z lazy initialization
function useLocalStorage(klucz, wartoscDomyslna) {
  const [wartosc, setWartosc] = useState(() => {
    try {
      const zapisana = localStorage.getItem(klucz);
      return zapisana ? JSON.parse(zapisana) : wartoscDomyslna;
    } catch {
      return wartoscDomyslna;
    }
  });

  const zapiszWartosc = (nowaWartosc) => {
    setWartosc(nowaWartosc);
    localStorage.setItem(klucz, JSON.stringify(nowaWartosc));
  };

  return [wartosc, zapiszWartosc];
}

// Użycie custom hooka
function KomponentZCustomHook() {
  const [licznik, setLicznik] = useLocalStorage('licznik', 0);

  return (
    <div>
      <p>Licznik: {licznik}</p>
      <button onClick={() => setLicznik(licznik + 1)}>
        Zwiększ
      </button>
    </div>
  );
}

Materiały

React Hooks - Kategoria 3: useEffect

Do czego służy Hook useEffect i kiedy jest wywoływany?

Odpowiedź w 30 sekund: Hook useEffect służy do wykonywania efektów ubocznych (side effects) w komponentach funkcyjnych React. Jest wywoływany po każdym renderze komponentu (domyślnie) i pozwala na wykonywanie operacji takich jak pobieranie danych, subskrypcje, manipulacja DOM czy logging. Zastępuje metody cyklu życia komponentów klasowych jak componentDidMount, componentDidUpdate i componentWillUnmount.

Odpowiedź w 2 minuty: useEffect to fundamentalny Hook w React, który umożliwia wykonywanie efektów ubocznych w komponentach funkcyjnych. Efekty uboczne to wszelkie operacje, które wychodzą poza zwykłe obliczenia wartości zwracanej przez komponent - na przykład zapytania HTTP, subskrypcje do zewnętrznych źródeł danych, timery, manipulacja DOM poza React czy aktualizacja tytułu strony.

Hook jest wywoływany po tym, jak React zaktualizuje DOM, co oznacza że efekt nie blokuje renderowania przeglądarki. To sprawia, że aplikacja jest bardziej responsywna. Domyślnie useEffect uruchamia się po każdym renderze (zarówno początkowym, jak i każdej aktualizacji), ale to zachowanie można kontrolować za pomocą tablicy zależności.

useEffect przyjmuje dwa argumenty: funkcję efektu oraz opcjonalną tablicę zależności. Funkcja efektu może zwracać funkcję czyszczącą (cleanup function), która zostanie wywołana przed następnym uruchomieniem efektu lub przed odmontowaniem komponentu. To pozwala na czyszczenie zasobów jak subskrypcje czy timery, zapobiegając wyciekom pamięci.

Dzięki useEffect możemy przenieść logikę związaną z efektami ubocznymi do jednego miejsca, zamiast rozpraszać ją między różne metody cyklu życia jak w komponentach klasowych. To sprawia, że kod jest bardziej czytelny i łatwiejszy w utrzymaniu.

Przykład kodu:

import { useState, useEffect } from 'react';

function TytulStrony() {
  const [licznik, setLicznik] = useState(0);

  // Efekt uruchamiany po każdym renderze
  useEffect(() => {
    // Aktualizacja tytułu dokumentu
    document.title = `Kliknięto ${licznik} razy`;
    console.log('Efekt wykonany dla licznika:', licznik);
  });

  return (
    <div>
      <p>Kliknięto {licznik} razy</p>
      <button onClick={() => setLicznik(licznik + 1)}>
        Kliknij mnie
      </button>
    </div>
  );
}

// Przykład z pobieraniem danych
function ProfilUzytkownika({ userId }) {
  const [uzytkownik, setUzytkownik] = useState(null);
  const [ladowanie, setLadowanie] = useState(true);

  useEffect(() => {
    setLadowanie(true);

    // Pobieranie danych użytkownika
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(dane => {
        setUzytkownik(dane);
        setLadowanie(false);
      })
      .catch(error => {
        console.error('Błąd pobierania:', error);
        setLadowanie(false);
      });
  }, [userId]); // Efekt uruchomi się ponownie gdy zmieni się userId

  if (ladowanie) return <div>Ładowanie...</div>;
  if (!uzytkownik) return <div>Nie znaleziono użytkownika</div>;

  return (
    <div>
      <h2>{uzytkownik.imie}</h2>
      <p>{uzytkownik.email}</p>
    </div>
  );
}

Diagram cyklu życia useEffect:

graph TD
    A[Komponent renderuje się] --> B[React aktualizuje DOM]
    B --> C[Przeglądarka maluje ekran]
    C --> D[useEffect uruchamia się]
    D --> E{Czy są zależności?}
    E -->|Tak| F{Czy zależności się zmieniły?}
    E -->|Nie| G[Uruchom przy każdym renderze]
    F -->|Tak| H[Uruchom efekt]
    F -->|Nie| I[Pomiń efekt]
    H --> J{Czy zwrócono cleanup?}
    G --> J
    J -->|Tak| K[Zapisz funkcję cleanup]
    J -->|Nie| L[Koniec]
    K --> L
    M[Przed następnym efektem lub odmontowaniem] --> N[Wykonaj cleanup]
    N --> H

Materiały:

Czym jest tablica zależności w useEffect i jak wpływa na działanie efektu?

Odpowiedź w 30 sekund: Tablica zależności to drugi opcjonalny argument useEffect, który kontroluje kiedy efekt ma się ponownie uruchomić. React porównuje wartości w tablicy z poprzedniego renderowania z aktualnymi - jeśli którakolwiek wartość się zmieniła, efekt zostanie wykonany ponownie. Pusta tablica [] oznacza uruchomienie tylko raz, a brak tablicy powoduje uruchomienie po każdym renderze.

Odpowiedź w 2 minuty: Tablica zależności (dependency array) jest kluczowym mechanizmem optymalizacji w useEffect. Pozwala precyzyjnie kontrolować, kiedy efekt powinien się ponownie wykonać. React używa algorytmu Object.is() do porównywania wartości z poprzedniego renderowania z aktualnymi wartościami w tablicy.

Jeśli w tablicy umieścimy zmienne state, props lub inne wartości, efekt wykona się tylko wtedy, gdy którakolwiek z tych wartości ulegnie zmianie. To zapobiega niepotrzebnemu wykonywaniu kosztownych operacji jak zapytania API czy skomplikowane obliczenia. Bez tablicy zależności efekt uruchamiałby się po każdej aktualizacji komponentu, co mogłoby prowadzić do problemów z wydajnością.

Ważne jest, aby uwzględnić w tablicy wszystkie wartości z zakresu komponentu (scope), które są używane wewnątrz efektu. Pominięcie zależności może prowadzić do bugów, gdzie efekt operuje na przestarzałych wartościach (stale closure). ESLint z pluginem react-hooks pomaga wykrywać brakujące zależności poprzez regułę exhaustive-deps.

Specjalne przypadki tablicy zależności: pusta tablica [] działa jak componentDidMount (uruchomienie tylko raz po montowaniu), brak tablicy oznacza uruchomienie po każdym renderze, a tablica z zależnościami działa jak kontrolowany componentDidUpdate.

Przykład kodu:

import { useState, useEffect } from 'react';

function PrzykladyZaleznosci() {
  const [licznik1, setLicznik1] = useState(0);
  const [licznik2, setLicznik2] = useState(0);
  const [tekst, setTekst] = useState('');

  // 1. Brak tablicy - uruchamia się po KAŻDYM renderze
  useEffect(() => {
    console.log('Efekt 1: Każdy render');
  });

  // 2. Pusta tablica - uruchamia się TYLKO RAZ (montowanie)
  useEffect(() => {
    console.log('Efekt 2: Tylko raz przy montowaniu');

    // Przykład: ustawienie tytułu strony
    document.title = 'Witaj w aplikacji!';
  }, []);

  // 3. Konkretna zależność - tylko gdy licznik1 się zmienia
  useEffect(() => {
    console.log('Efekt 3: licznik1 się zmienił:', licznik1);

    // Przykład: zapisywanie do localStorage
    localStorage.setItem('licznik1', licznik1.toString());
  }, [licznik1]);

  // 4. Wiele zależności - gdy zmieni się którakolwiek
  useEffect(() => {
    console.log('Efekt 4: licznik1 lub licznik2 się zmienił');

    const suma = licznik1 + licznik2;
    document.title = `Suma: ${suma}`;
  }, [licznik1, licznik2]);

  // 5. Złożony przykład z funkcją w zależnościach
  const wyszukaj = (zapytanie) => {
    console.log('Wyszukiwanie:', zapytanie);
  };

  useEffect(() => {
    if (tekst.length > 2) {
      // Opóźnienie wyszukiwania (debouncing)
      const timer = setTimeout(() => {
        wyszukaj(tekst);
      }, 500);

      // Cleanup: anulowanie poprzedniego timera
      return () => clearTimeout(timer);
    }
  }, [tekst]); // tekst jest zależnością

  return (
    <div>
      <div>
        <button onClick={() => setLicznik1(licznik1 + 1)}>
          Licznik 1: {licznik1}
        </button>
        <button onClick={() => setLicznik2(licznik2 + 1)}>
          Licznik 2: {licznik2}
        </button>
      </div>
      <input
        type="text"
        value={tekst}
        onChange={(e) => setTekst(e.target.value)}
        placeholder="Wpisz tekst do wyszukania..."
      />
    </div>
  );
}

// Przykład z obiektami jako zależnościami (potencjalna pułapka!)
function PulapkaObiektow({ config }) {
  useEffect(() => {
    // PROBLEM: jeśli config jest tworzony na nowo przy każdym renderze
    // efekt uruchomi się za każdym razem, nawet jeśli wartości są takie same
    console.log('Konfiguracja się zmieniła:', config);
  }, [config]); // config jako zależność

  return <div>Zobacz konsolę</div>;
}

// Lepsze rozwiązanie - konkretne właściwości jako zależności
function LepszeRozwiazanie({ config }) {
  useEffect(() => {
    console.log('API URL się zmienił:', config.apiUrl);
  }, [config.apiUrl]); // Tylko konkretna właściwość

  return <div>Zobacz konsolę</div>;
}

Diagram przepływu tablicy zależności:

graph TD
    A[Komponent renderuje się] --> B{Czy useEffect ma tablicę zależności?}
    B -->|Nie| C[Uruchom efekt po każdym renderze]
    B -->|Tak, pusta []| D{Czy to pierwszy render?}
    B -->|Tak, z wartościami| E{Czy to pierwszy render?}
    D -->|Tak| F[Uruchom efekt]
    D -->|Nie| G[Pomiń efekt]
    E -->|Tak| H[Uruchom efekt]
    E -->|Nie| I[Porównaj zależności z poprzednim renderem]
    I --> J{Czy któraś wartość się zmieniła?}
    J -->|Tak| K[Uruchom cleanup jeśli istnieje]
    J -->|Nie| L[Pomiń efekt]
    K --> M[Uruchom efekt]
    C --> N[Koniec]
    F --> N
    G --> N
    H --> N
    L --> N
    M --> N

Materiały:

Jak prawidłowo wykonać cleanup w useEffect?

Odpowiedź w 30 sekund: Cleanup w useEffect wykonuje się poprzez zwrócenie funkcji z efektu. Ta funkcja zostanie wywołana przed następnym uruchomieniem efektu lub przed odmontowaniem komponentu. Cleanup jest niezbędny do czyszczenia zasobów jak subskrypcje, timery, połączenia WebSocket czy event listenery, zapobiegając wyciekom pamięci i niechcianym efektom ubocznym.

Odpowiedź w 2 minuty: Funkcja cleanup (czyszcząca) to mechanizm pozwalający na bezpieczne zarządzanie zasobami w React. Zwracana z useEffect funkcja będzie automatycznie wywołana w dwóch sytuacjach: przed ponownym uruchomieniem efektu (gdy zależności się zmienią) oraz przed odmontowaniem komponentu. To zapewnia, że nie pozostawiamy aktywnych subskrypcji, timerów czy listenerów po usunięciu komponentu z drzewa.

Cleanup jest szczególnie istotny przy operacjach asynchronicznych. Jeśli komponent zostanie odmontowany przed zakończeniem operacji (np. request HTTP), cleanup pozwala anulować tę operację lub zignorować jej wynik, zapobiegając próbom aktualizacji stanu w odmontowanym komponencie (co powoduje ostrzeżenie "Can't perform a React state update on an unmounted component").

Najczęstsze przypadki użycia cleanup to: anulowanie subskrypcji (WebSocket, EventSource), usuwanie event listenerów z DOM, anulowanie timerów (setTimeout, setInterval), anulowanie requestów HTTP (AbortController), zamykanie połączeń z bazą danych czy czyszczenie bibliotek zewnętrznych (np. wykresy, mapy).

Ważne jest, aby cleanup wykonywał operacje odwrotne do tych z efektu - jeśli efekt coś dodaje, cleanup powinien to usunąć. Jeśli efekt coś subskrybuje, cleanup powinien anulować subskrypcję. Taka symetria zapewnia brak wycieków pamięci i poprawne działanie aplikacji.

Przykład kodu:

import { useState, useEffect } from 'react';

// 1. Cleanup dla timerów
function Stoper() {
  const [sekundy, setSekundy] = useState(0);
  const [aktywny, setAktywny] = useState(false);

  useEffect(() => {
    if (aktywny) {
      // Uruchomienie interwału
      const intervalId = setInterval(() => {
        setSekundy(prev => prev + 1);
      }, 1000);

      // Cleanup: zatrzymanie interwału
      return () => {
        console.log('Czyszczenie interwału');
        clearInterval(intervalId);
      };
    }
  }, [aktywny]);

  return (
    <div>
      <p>Sekundy: {sekundy}</p>
      <button onClick={() => setAktywny(!aktywny)}>
        {aktywny ? 'Stop' : 'Start'}
      </button>
    </div>
  );
}

// 2. Cleanup dla event listenerów
function RozmiaryOkna() {
  const [szerokosc, setSzerokosc] = useState(window.innerWidth);

  useEffect(() => {
    // Funkcja obsługi zdarzenia
    const obslugaZmianyRozmiaru = () => {
      setSzerokosc(window.innerWidth);
    };

    // Dodanie listenera
    window.addEventListener('resize', obslugaZmianyRozmiaru);

    // Cleanup: usunięcie listenera
    return () => {
      console.log('Usuwanie event listenera');
      window.removeEventListener('resize', obslugaZmianyRozmiaru);
    };
  }, []); // Pusta tablica - tylko przy montowaniu/odmontowaniu

  return <div>Szerokość okna: {szerokosc}px</div>;
}

// 3. Cleanup dla requestów HTTP (AbortController)
function PobieranieUzytkownika({ userId }) {
  const [uzytkownik, setUzytkownik] = useState(null);
  const [ladowanie, setLadowanie] = useState(true);
  const [blad, setBlad] = useState(null);

  useEffect(() => {
    // Utworzenie kontrolera do anulowania requestu
    const abortController = new AbortController();

    setLadowanie(true);
    setBlad(null);

    fetch(`https://api.example.com/users/${userId}`, {
      signal: abortController.signal
    })
      .then(response => response.json())
      .then(dane => {
        setUzytkownik(dane);
        setLadowanie(false);
      })
      .catch(error => {
        // Ignoruj błędy anulowania
        if (error.name === 'AbortError') {
          console.log('Request anulowany');
        } else {
          setBlad(error.message);
          setLadowanie(false);
        }
      });

    // Cleanup: anulowanie requestu
    return () => {
      console.log('Anulowanie requestu dla użytkownika:', userId);
      abortController.abort();
    };
  }, [userId]);

  if (ladowanie) return <div>Ładowanie...</div>;
  if (blad) return <div>Błąd: {blad}</div>;
  if (!uzytkownik) return null;

  return <div>{uzytkownik.imie}</div>;
}

// 4. Cleanup dla subskrypcji (WebSocket)
function CzatNaZywo({ pokoj }) {
  const [wiadomosci, setWiadomosci] = useState([]);

  useEffect(() => {
    // Połączenie z WebSocket
    const ws = new WebSocket(`wss://chat.example.com/${pokoj}`);

    ws.onopen = () => {
      console.log('Połączono z pokojem:', pokoj);
    };

    ws.onmessage = (event) => {
      const wiadomosc = JSON.parse(event.data);
      setWiadomosci(prev => [...prev, wiadomosc]);
    };

    ws.onerror = (error) => {
      console.error('Błąd WebSocket:', error);
    };

    // Cleanup: zamknięcie połączenia
    return () => {
      console.log('Rozłączanie z pokojem:', pokoj);
      ws.close();
    };
  }, [pokoj]);

  return (
    <div>
      <h3>Pokój: {pokoj}</h3>
      <ul>
        {wiadomosci.map((wiad, index) => (
          <li key={index}>{wiad.tekst}</li>
        ))}
      </ul>
    </div>
  );
}

// 5. Cleanup dla bibliotek zewnętrznych (np. wykres)
function Wykres({ dane }) {
  useEffect(() => {
    // Inicjalizacja wykresu (przykład z Chart.js)
    const ctx = document.getElementById('wykres').getContext('2d');
    const wykres = new Chart(ctx, {
      type: 'line',
      data: dane,
      options: { responsive: true }
    });

    // Cleanup: zniszczenie instancji wykresu
    return () => {
      console.log('Niszczenie wykresu');
      wykres.destroy();
    };
  }, [dane]);

  return <canvas id="wykres"></canvas>;
}

Diagram procesu cleanup:

sequenceDiagram
    participant Component as Komponent
    participant React
    participant Effect as useEffect
    participant Cleanup as Funkcja Cleanup

    Note over Component,Cleanup: Pierwszy render
    Component->>React: Render
    React->>Effect: Uruchom efekt
    Effect-->>React: Zwróć funkcję cleanup
    React->>React: Zapisz cleanup

    Note over Component,Cleanup: Zmiana zależności
    Component->>React: Re-render
    React->>Cleanup: Wykonaj poprzedni cleanup
    React->>Effect: Uruchom nowy efekt
    Effect-->>React: Zwróć nową funkcję cleanup

    Note over Component,Cleanup: Odmontowanie
    Component->>React: Unmount
    React->>Cleanup: Wykonaj cleanup
    Note over Cleanup: Czyszczenie zasobów

Materiały:

Jaka jest różnica między pustą tablicą zależności a brakiem tablicy zależności?

Odpowiedź w 30 sekund: Pusta tablica zależności [] powoduje wykonanie efektu tylko raz po montowaniu komponentu (odpowiednik componentDidMount). Brak tablicy zależności powoduje wykonanie efektu po każdym renderze komponentu (po montowaniu i każdej aktualizacji). To fundamentalna różnica wpływająca na wydajność i zachowanie aplikacji.

Odpowiedź w 2 minuty: Różnica między pustą tablicą [] a brakiem tablicy jest kluczowa dla zrozumienia kontroli nad useEffect. Pusta tablica oznacza "nie ma zależności do śledzenia", więc React uruchamia efekt tylko raz - po pierwszym renderze. Jest to idealne dla operacji inicjalizacyjnych jak pobieranie początkowych danych, ustawienie subskrypcji czy konfiguracja bibliotek zewnętrznych.

Brak tablicy natomiast oznacza "brak optymalizacji" - React uruchamia efekt po każdym renderze, niezależnie od tego, co się zmieniło. To może prowadzić do problemów z wydajnością, szczególnie jeśli efekt wykonuje kosztowne operacje. Jest to rzadko pożądane zachowanie i zazwyczaj wskazuje na błąd w kodzie.

W praktyce, większość efektów powinna mieć określoną tablicę zależności z konkretnymi wartościami. Pusta tablica jest odpowiednia dla operacji "raz na komponent", natomiast brak tablicy może być uzasadniony tylko w specyficznych przypadkach, jak debugowanie czy synchronizacja z zewnętrznymi systemami, które nie korzystają z React.

Ważne jest też zrozumienie, że pusta tablica nie oznacza "nigdy się nie uruchamia ponownie" - funkcja cleanup nadal zostanie wywołana przy odmontowaniu komponentu. To pozwala na poprawne czyszczenie zasobów nawet przy efektach "jednorazowych".

Przykład kodu:

import { useState, useEffect } from 'react';

function PorownanieTablicZaleznosci() {
  const [licznik, setLicznik] = useState(0);
  const [liczbaRenderow, setLiczbaRenderow] = useState(0);

  // 1. BRAK TABLICY - uruchamia się po KAŻDYM renderze
  useEffect(() => {
    console.log('❌ Brak tablicy - wykonuje się za każdym razem');
    console.log('Liczba renderów:', liczbaRenderow + 1);

    // Ten efekt uruchomi się przy każdej zmianie licznika
    // lub jakiejkolwiek innej zmiany stanu!
  });

  // 2. PUSTA TABLICA - uruchamia się tylko RAZ
  useEffect(() => {
    console.log('✅ Pusta tablica [] - wykonuje się tylko raz');
    console.log('Komponent został zamontowany');

    // Idealny do inicjalizacji
    const startTime = Date.now();

    // Cleanup nadal działa przy odmontowaniu
    return () => {
      const endTime = Date.now();
      console.log(`Komponent żył ${(endTime - startTime) / 1000} sekund`);
    };
  }, []);

  // Licznik renderów (używa pustej tablicy, więc nie wpływa na licznik)
  useEffect(() => {
    setLiczbaRenderow(prev => prev + 1);
  });

  return (
    <div>
      <p>Licznik: {licznik}</p>
      <p>Liczba renderów: {liczbaRenderow}</p>
      <button onClick={() => setLicznik(licznik + 1)}>
        Zwiększ licznik
      </button>
    </div>
  );
}

// Przykład praktyczny: pobieranie danych
function ListaUzytkownikow() {
  const [uzytkownicy, setUzytkownicy] = useState([]);
  const [filtr, setFiltr] = useState('');
  const [ladowanie, setLadowanie] = useState(true);

  // PUSTA TABLICA - pobierz dane tylko raz
  useEffect(() => {
    console.log('Pobieranie wszystkich użytkowników (tylko raz)');

    fetch('https://api.example.com/users')
      .then(res => res.json())
      .then(dane => {
        setUzytkownicy(dane);
        setLadowanie(false);
      });

    // Ten efekt NIE uruchomi się ponownie gdy zmieni się filtr
    // Dane są pobierane tylko raz przy montowaniu
  }, []);

  // BRAK TABLICY - wykona się przy każdej zmianie filtra
  // (niepotrzebnie! - to przykład złej praktyki)
  // useEffect(() => {
  //   console.log('⚠️ To uruchomi się za każdym razem!');
  //   document.title = `Filtr: ${filtr}`;
  // });

  // PRAWIDŁOWE - konkretna zależność
  useEffect(() => {
    console.log('Filtr się zmienił:', filtr);
    document.title = `Filtr: ${filtr}`;
  }, [filtr]); // Tylko gdy filtr się zmieni

  if (ladowanie) return <div>Ładowanie...</div>;

  const przefiltrwaniUzytkownicy = uzytkownicy.filter(u =>
    u.imie.toLowerCase().includes(filtr.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={filtr}
        onChange={(e) => setFiltr(e.target.value)}
        placeholder="Filtruj użytkowników..."
      />
      <ul>
        {przefiltrwaniUzytkownicy.map(u => (
          <li key={u.id}>{u.imie}</li>
        ))}
      </ul>
    </div>
  );
}

// Przykład pokazujący problem z brakiem tablicy
function ProblematycznyKomponent() {
  const [dane, setDane] = useState([]);
  const [licznik, setLicznik] = useState(0);

  // ⚠️ ZŁA PRAKTYKA - brak tablicy
  useEffect(() => {
    console.log('Pobieranie danych...');

    // To będzie wykonywane po KAŻDYM renderze!
    // Nawet gdy licznik się zmieni, pobieramy dane ponownie
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(setDane);

    // To może spowodować:
    // - Niepotrzebne zapytania API
    // - Problemy z wydajnością
    // - Przekroczenie limitów API
    // - Zwiększone koszty
  });

  return (
    <div>
      <button onClick={() => setLicznik(licznik + 1)}>
        Licznik: {licznik}
      </button>
      <p>Dane: {dane.length} elementów</p>
    </div>
  );
}

// Poprawna wersja
function PoprawnyKomponent() {
  const [dane, setDane] = useState([]);
  const [licznik, setLicznik] = useState(0);

  // ✅ DOBRA PRAKTYKA - pusta tablica dla jednorazowego pobrania
  useEffect(() => {
    console.log('Pobieranie danych (tylko raz)');

    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(setDane);
  }, []); // Tylko przy montowaniu

  return (
    <div>
      <button onClick={() => setLicznik(licznik + 1)}>
        Licznik: {licznik}
      </button>
      <p>Dane: {dane.length} elementów</p>
    </div>
  );
}

Porównanie wizualne:

graph TD
    A[useEffect Setup] --> B{Tablica zależności?}

    B -->|Brak tablicy| C[Każdy render]
    C --> C1[Mount]
    C --> C2[Update 1]
    C --> C3[Update 2]
    C --> C4[Update 3]
    C --> C5[...]

    B -->|Pusta tablica []| D[Tylko raz]
    D --> D1[Mount]
    D --> D2[❌ Update 1]
    D --> D3[❌ Update 2]

    B -->|Z wartościami [a, b]| E[Przy zmianie]
    E --> E1[Mount]
    E --> E2[✅ Zmiana a]
    E --> E3[❌ Inne zmiany]
    E --> E4[✅ Zmiana b]

    style C fill:#ffcccc
    style D fill:#ccffcc
    style E fill:#cce5ff

Tabela porównawcza:

Aspekt Brak tablicy Pusta tablica [] Tablica [dep1, dep2]
Kiedy się wykonuje Po każdym renderze Tylko przy montowaniu Przy zmianie zależności
Liczba wykonań n+1 (n = liczba aktualizacji) 1 1 + liczba zmian zależności
Wydajność ⚠️ Niska ✅ Wysoka ✅ Optymalna
Przypadki użycia Debugowanie, logging Inicjalizacja, subskrypcje Reakcja na zmiany
Cleanup Przed każdym efektem + unmount Tylko przy unmount Przed każdym efektem + unmount
Częstość użycia Rzadko (1-2%) Często (30-40%) Najczęściej (60-70%)

Materiały:

Jak uniknąć nieskończonej pętli w useEffect?

Odpowiedź w 30 sekund: Nieskończona pętla w useEffect występuje gdy efekt aktualizuje stan, który jest zależnością tego samego efektu. Można jej uniknąć przez: prawidłowe definiowanie tablicy zależności, używanie funkcji aktualizującej stan (functional update), stosowanie useCallback/useMemo dla obiektów i funkcji w zależnościach, oraz unikanie aktualizacji stanu w efektach bez kontroli.

Odpowiedź w 2 minuty: Nieskończona pętla jest jednym z najczęstszych problemów przy pracy z useEffect i może szybko doprowadzić do zawieszenia przeglądarki lub przekroczenia limitów API. Występuje gdy: (1) efekt aktualizuje stan, który jest jego zależnością, (2) obiekt lub funkcja w zależnościach są tworzone na nowo przy każdym renderze, (3) brakuje tablicy zależności przy efekcie modyfikującym stan.

Kluczem do uniknięcia pętli jest zrozumienie, że React porównuje zależności za pomocą Object.is(), co oznacza porównanie przez referencję dla obiektów i tablic. Jeśli obiekt jest tworzony na nowo przy każdym renderze (nawet z takimi samymi wartościami), React uzna go za zmieniony i uruchomi efekt ponownie.

Najlepsze praktyki to: (1) używać primitive values (string, number, boolean) jako zależności gdy to możliwe, (2) stosować funkcyjną formę setState (prev => prev + 1) aby nie musieć dodawać bieżącej wartości stanu do zależności, (3) opakowywać funkcje w useCallback i obiekty w useMemo gdy muszą być zależnościami, (4) rozważyć czy efekt w ogóle potrzebuje danej zależności.

Podczas debugowania pętli warto użyć console.log wewnątrz efektu lub React DevTools Profiler, aby zobaczyć dlaczego efekt uruchamia się wielokrotnie. ESLint z regułą react-hooks/exhaustive-deps pomoże wykryć potencjalne problemy, ale czasami trzeba świadomie zignorować jej ostrzeżenia (z komentarzem wyjaśniającym dlaczego).

Przykład kodu:

import { useState, useEffect, useCallback, useMemo } from 'react';

// ❌ PROBLEM 1: Nieskończona pętla - aktualizacja zależności w efekcie
function NieskonczonaPetla() {
  const [licznik, setLicznik] = useState(0);

  useEffect(() => {
    // Efekt aktualizuje licznik, który jest zależnością
    setLicznik(licznik + 1); // To spowoduje ponowne uruchomienie efektu!
  }, [licznik]); // licznik zmienia się -> efekt uruchamia się -> licznik zmienia się...

  return <div>Licznik: {licznik}</div>;
}

// ✅ ROZWIĄZANIE 1: Funkcyjna aktualizacja stanu
function PoprawneLiczenie() {
  const [licznik, setLicznik] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // Używamy funkcji, więc nie potrzebujemy licznik w zależnościach
      setLicznik(prev => prev + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []); // Pusta tablica - brak problemu z pętlą

  return <div>Licznik: {licznik}</div>;
}

// ❌ PROBLEM 2: Obiekt jako zależność (tworzony na nowo przy każdym renderze)
function ProblematycznyObiekt({ userId }) {
  const [dane, setDane] = useState(null);

  // Obiekt tworzony przy każdym renderze - nowa referencja!
  const konfiguracja = {
    userId: userId,
    format: 'json'
  };

  useEffect(() => {
    fetch(`/api/users/${konfiguracja.userId}`)
      .then(res => res.json())
      .then(setDane);
  }, [konfiguracja]); // konfiguracja zawsze się "zmienia" -> pętla!

  return <div>{dane?.imie}</div>;
}

// ✅ ROZWIĄZANIE 2a: Użyj primitive values jako zależności
function PoprawneZaleznosci({ userId }) {
  const [dane, setDane] = useState(null);

  useEffect(() => {
    // Używaj bezpośrednio wartości, nie obiektu
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setDane);
  }, [userId]); // userId to string/number - stabilna zależność

  return <div>{dane?.imie}</div>;
}

// ✅ ROZWIĄZANIE 2b: Użyj useMemo dla obiektów
function PoprawnyObiektZMemo({ userId, format }) {
  const [dane, setDane] = useState(null);

  // Obiekt będzie ponownie utworzony tylko gdy zmieni się userId lub format
  const konfiguracja = useMemo(() => ({
    userId: userId,
    format: format
  }), [userId, format]);

  useEffect(() => {
    console.log('Pobieranie z konfiguracją:', konfiguracja);
    fetch(`/api/users/${konfiguracja.userId}`)
      .then(res => res.json())
      .then(setDane);
  }, [konfiguracja]); // Teraz konfiguracja jest stabilna

  return <div>{dane?.imie}</div>;
}

// ❌ PROBLEM 3: Funkcja jako zależność (tworzona na nowo)
function ProblematycznaFunkcja({ userId }) {
  const [dane, setDane] = useState(null);

  // Funkcja tworzona przy każdym renderze
  const pobierzDane = () => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setDane);
  };

  useEffect(() => {
    pobierzDane();
  }, [pobierzDane]); // pobierzDane zawsze nowa referencja -> pętla!

  return <div>{dane?.imie}</div>;
}

// ✅ ROZWIĄZANIE 3: Użyj useCallback dla funkcji
function PoprawnaFunkcjaZCallback({ userId }) {
  const [dane, setDane] = useState(null);

  // Funkcja będzie ponownie utworzona tylko gdy zmieni się userId
  const pobierzDane = useCallback(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setDane);
  }, [userId]);

  useEffect(() => {
    pobierzDane();
  }, [pobierzDane]); // Teraz pobierzDane jest stabilna

  return <div>{dane?.imie}</div>;
}

// ✅ ROZWIĄZANIE 3 ALTERNATYWNE: Przenieś funkcję do efektu
function NajlepszeRozwiazanie({ userId }) {
  const [dane, setDane] = useState(null);

  useEffect(() => {
    // Funkcja zdefiniowana wewnątrz efektu - nie potrzeba w zależnościach
    const pobierzDane = () => {
      fetch(`/api/users/${userId}`)
        .then(res => res.json())
        .then(setDane);
    };

    pobierzDane();
  }, [userId]); // Tylko userId jako zależność

  return <div>{dane?.imie}</div>;
}

// ❌ PROBLEM 4: Aktualizacja na podstawie poprzedniej wartości
function ProblematycznyFormularz() {
  const [formularz, setFormularz] = useState({ imie: '', email: '' });

  useEffect(() => {
    // Próba walidacji przy każdej zmianie
    if (formularz.imie.length < 3) {
      // To spowoduje pętlę jeśli modyfikujemy formularz
      setFormularz({ ...formularz, bladImienia: true });
    }
  }, [formularz]); // formularz się zmienia -> efekt -> formularz się zmienia...

  return <input value={formularz.imie} />;
}

// ✅ ROZWIĄZANIE 4: Osobny stan dla walidacji
function PoprawnyFormularz() {
  const [formularz, setFormularz] = useState({ imie: '', email: '' });
  const [bledy, setBledy] = useState({});

  useEffect(() => {
    // Walidacja zapisywana w osobnym stanie
    const noweBledy = {};
    if (formularz.imie.length < 3) {
      noweBledy.imie = 'Imię zbyt krótkie';
    }
    setBledy(noweBledy);
  }, [formularz.imie, formularz.email]); // Tylko konkretne pola

  return (
    <div>
      <input
        value={formularz.imie}
        onChange={e => setFormularz({ ...formularz, imie: e.target.value })}
      />
      {bledy.imie && <span>{bledy.imie}</span>}
    </div>
  );
}

// Narzędzie debugowania - wykrycie pętli
function DebugUseEffect({ zaleznosci }) {
  const [licznik, setLicznik] = useState(0);
  const poprzednieZaleznosci = useRef();

  useEffect(() => {
    setLicznik(prev => prev + 1);

    if (licznik > 100) {
      console.error('⚠️ MOŻLIWA NIESKOŃCZONA PĘTLA!');
      console.log('Poprzednie zależności:', poprzednieZaleznosci.current);
      console.log('Aktualne zależności:', zaleznosci);
    }

    poprzednieZaleznosci.current = zaleznosci;
  }, [zaleznosci]);

  return <div>Licznik renderów: {licznik}</div>;
}

Diagram przyczyn i rozwiązań:

graph TD
    A[Nieskończona pętla w useEffect] --> B{Przyczyna}

    B --> C[Stan w zależnościach<br/>aktualizowany w efekcie]
    B --> D[Obiekt/tablica tworzony<br/>przy każdym renderze]
    B --> E[Funkcja tworzona<br/>przy każdym renderze]
    B --> F[Brak tablicy zależności<br/>przy aktualizacji stanu]

    C --> C1[✅ Użyj setState(prev => ...)]
    C --> C2[✅ Usuń stan z zależności<br/>jeśli możliwe]

    D --> D1[✅ Użyj useMemo]
    D --> D2[✅ Użyj primitive values]
    D --> D3[✅ Przenieś obiekt poza komponent]

    E --> E1[✅ Użyj useCallback]
    E --> E2[✅ Zdefiniuj funkcję<br/>wewnątrz useEffect]

    F --> F1[✅ Dodaj pustą tablicę []]
    F --> F2[✅ Dodaj konkretne zależności]

    style A fill:#ffcccc
    style C1 fill:#ccffcc
    style C2 fill:#ccffcc
    style D1 fill:#ccffcc
    style D2 fill:#ccffcc
    style D3 fill:#ccffcc
    style E1 fill:#ccffcc
    style E2 fill:#ccffcc
    style F1 fill:#ccffcc
    style F2 fill:#ccffcc

Checklist zapobiegania pętlom:

✅ Używam funkcyjnej aktualizacji stanu: setState(prev => prev + 1)
✅ Zależności to primitive values (string, number, boolean)
✅ Obiekty w zależnościach są opakowane w useMemo
✅ Funkcje w zależnościach są opakowane w useCallback
✅ Sprawdziłem czy wszystkie zależności są rzeczywiście potrzebne
✅ Funkcje używane tylko w efekcie są zdefiniowane wewnątrz niego
✅ Włączyłem ESLint z regułą react-hooks/exhaustive-deps
✅ Testuję czy liczba wywołań efektu jest rozsądna

Materiały:

Jak wykonać efekt tylko przy pierwszym renderze (componentDidMount)?

Odpowiedź w 30 sekund: Aby wykonać efekt tylko przy pierwszym renderze (odpowiednik componentDidMount z klas), należy użyć useEffect z pustą tablicą zależności []. To gwarantuje, że efekt uruchomi się tylko raz po zamontowaniu komponentu. Funkcja cleanup zwrócona z takiego efektu zostanie wywołana tylko przy odmontowaniu (odpowiednik componentWillUnmount).

Odpowiedź w 2 minuty: Pusta tablica zależności [] w useEffect to idiom React oznaczający "nie ma wartości do śledzenia", co sprawia że efekt wykonuje się tylko raz - po pierwszym renderze komponentu. Jest to bezpośredni odpowiednik metody componentDidMount z komponentów klasowych i jest idealny do operacji inicjalizacyjnych.

Typowe przypadki użycia to: pobieranie początkowych danych z API, konfiguracja subskrypcji (WebSocket, EventSource), inicjalizacja bibliotek zewnętrznych (wykresy, mapy, edytory), rejestracja globalnych event listenerów, ustawienie początkowej konfiguracji (np. tytuł strony), czy logowanie analityczne (śledzenie odsłon strony).

Ważne jest zrozumienie, że mimo iż efekt uruchamia się tylko raz, wartości props i state wewnątrz efektu są "zamrożone" z momentu pierwszego renderowania. Jeśli potrzebujesz dostępu do aktualnych wartości, musisz użyć ref lub dodać te wartości do tablicy zależności (co spowoduje uruchomienie efektu przy ich zmianie).

Funkcja cleanup zwrócona z efektu z pustą tablicą zostanie wywołana tylko przy odmontowaniu komponentu, co odpowiada componentWillUnmount. To idealne miejsce na czyszczenie subskrypcji, timerów czy event listenerów utworzonych w efekcie inicjalizacyjnym.

W trybie Strict Mode (React 18+) efekty są uruchamiane dwukrotnie w developmencie, aby pomóc wykryć błędy z brakiem cleanup. To zamierzone zachowanie i nie występuje w produkcji.

Przykład kodu:

import { useState, useEffect, useRef } from 'react';

// Podstawowy przykład - inicjalizacja jednorazowa
function KomponentZInicjalizacja() {
  const [dane, setDane] = useState(null);
  const [ladowanie, setLadowanie] = useState(true);

  // useEffect z pustą tablicą [] = componentDidMount
  useEffect(() => {
    console.log('Komponent zamontowany - wykonuje się tylko RAZ');

    // Pobieranie początkowych danych
    fetch('https://api.example.com/initial-data')
      .then(res => res.json())
      .then(data => {
        setDane(data);
        setLadowanie(false);
      });

    // Ustawienie tytułu strony
    document.title = 'Moja Aplikacja';

    // Logowanie analityczne
    console.log('Strona odwiedzona:', new Date());

    // Cleanup - wykonuje się tylko przy odmontowaniu
    return () => {
      console.log('Komponent odmontowany - cleanup');
      document.title = 'React App'; // Przywrócenie domyślnego tytułu
    };
  }, []); // Pusta tablica = tylko przy montowaniu

  if (ladowanie) return <div>Ładowanie...</div>;
  return <div>Dane: {JSON.stringify(dane)}</div>;
}

// Przykład ze subskrypcją WebSocket
function CzasRzeczywisty() {
  const [wiadomosci, setWiadomosci] = useState([]);
  const wsRef = useRef(null);

  useEffect(() => {
    console.log('Inicjalizacja połączenia WebSocket');

    // Połączenie tworzone tylko raz
    wsRef.current = new WebSocket('wss://api.example.com/realtime');

    wsRef.current.onopen = () => {
      console.log('WebSocket połączony');
    };

    wsRef.current.onmessage = (event) => {
      const wiadomosc = JSON.parse(event.data);
      setWiadomosci(prev => [...prev, wiadomosc]);
    };

    wsRef.current.onerror = (error) => {
      console.error('Błąd WebSocket:', error);
    };

    // Cleanup - zamknięcie połączenia przy odmontowaniu
    return () => {
      console.log('Zamykanie połączenia WebSocket');
      if (wsRef.current) {
        wsRef.current.close();
      }
    };
  }, []); // Tylko przy montowaniu

  return (
    <div>
      <h3>Wiadomości w czasie rzeczywistym:</h3>
      <ul>
        {wiadomosci.map((w, i) => (
          <li key={i}>{w.tekst}</li>
        ))}
      </ul>
    </div>
  );
}

// Przykład z biblioteką zewnętrzną (mapa)
function KomponentMapy({ wspolrzedne }) {
  const mapaRef = useRef(null);
  const instancjaMapy = useRef(null);

  useEffect(() => {
    console.log('Inicjalizacja mapy');

    // Inicjalizacja mapy tylko raz
    // (przykład z Leaflet/Google Maps)
    instancjaMapy.current = new Map(mapaRef.current, {
      center: wspolrzedne,
      zoom: 13
    });

    // Dodanie kontrolek
    instancjaMapy.current.addControls();

    // Cleanup - zniszczenie instancji mapy
    return () => {
      console.log('Usuwanie mapy');
      if (instancjaMapy.current) {
        instancjaMapy.current.remove();
      }
    };
  }, []); // Tylko przy montowaniu

  // Osobny efekt dla aktualizacji współrzędnych
  useEffect(() => {
    if (instancjaMapy.current) {
      console.log('Aktualizacja współrzędnych mapy');
      instancjaMapy.current.setCenter(wspolrzedne);
    }
  }, [wspolrzedne]); // Uruchamia się gdy zmienią się współrzędne

  return <div ref={mapaRef} style={{ width: '100%', height: '400px' }} />;
}

// Przykład z globalnym event listenerem
function SkrotyKlawiaturowe() {
  const [nacisniety, setNacisniety] = useState('');

  useEffect(() => {
    console.log('Rejestracja globalnego listenera klawiatury');

    const obslugaKlawisza = (event) => {
      // Skróty klawiszowe (Ctrl + K, etc.)
      if (event.ctrlKey && event.key === 'k') {
        event.preventDefault();
        setNacisniety('Ctrl + K');
        console.log('Uruchomiono szukanie');
      }
    };

    // Dodanie listenera do dokumentu
    document.addEventListener('keydown', obslugaKlawisza);

    // Cleanup - usunięcie listenera
    return () => {
      console.log('Usuwanie globalnego listenera klawiatury');
      document.removeEventListener('keydown', obslugaKlawisza);
    };
  }, []); // Tylko przy montowaniu

  return (
    <div>
      <p>Naciśnij Ctrl + K</p>
      <p>Ostatni skrót: {nacisniety}</p>
    </div>
  );
}

// Przykład z pobieraniem danych i analitiką
function StronaProdukt({ produktId }) {
  const [produkt, setProdukt] = useState(null);
  const [ladowanie, setLadowanie] = useState(true);
  const czasStartu = useRef(Date.now());

  // Inicjalizacja jednorazowa
  useEffect(() => {
    console.log('Strona produktu załadowana');

    // Śledzenie analityczne
    analytics.logPageView('product-page', { produktId });

    // Ustawienie metadanych
    document.querySelector('meta[name="theme-color"]')
      ?.setAttribute('content', '#007bff');

    // Cleanup
    return () => {
      // Śledzenie czasu spędzonego na stronie
      const czasSpedzony = Date.now() - czasStartu.current;
      analytics.logTimeSpent('product-page', czasSpedzony);

      // Przywrócenie metadanych
      document.querySelector('meta[name="theme-color"]')
        ?.setAttribute('content', '#000000');
    };
  }, []); // Tylko przy montowaniu

  // Osobny efekt dla pobierania danych produktu (może się zmienić)
  useEffect(() => {
    setLadowanie(true);

    fetch(`https://api.example.com/products/${produktId}`)
      .then(res => res.json())
      .then(data => {
        setProdukt(data);
        setLadowanie(false);
      });
  }, [produktId]); // Gdy zmieni się produktId

  if (ladowanie) return <div>Ładowanie...</div>;
  return <div>{produkt?.nazwa}</div>;
}

// Przykład: Różnica między componentDidMount a wieloma renderami
function PorowanieZachowania() {
  const [licznik, setLicznik] = useState(0);
  const liczbaUruchomuienRef = useRef(0);

  // Ten efekt uruchomi się tylko RAZ (componentDidMount)
  useEffect(() => {
    liczbaUruchomuienRef.current += 1;
    console.log(`Efekt z [] wykonał się ${liczbaUruchomuienRef.current} raz(y)`);
    console.log('To wykona się TYLKO przy pierwszym renderze');

    // Wartość licznik jest "zamrożona" na 0
    console.log('Wartość licznik w efekcie:', licznik); // Zawsze 0!
  }, []);

  return (
    <div>
      <p>Licznik: {licznik}</p>
      <p>Efekt uruchomiony: {liczbaUruchomuienRef.current} raz(y)</p>
      <button onClick={() => setLicznik(licznik + 1)}>
        Zwiększ licznik
      </button>
      <p>
        Uwaga: Mimo że licznik się zmienia, efekt nie uruchamia się ponownie!
      </p>
    </div>
  );
}

// Obsługa React 18 Strict Mode (double mounting)
function ObslugaStrictMode() {
  useEffect(() => {
    console.log('Efekt uruchomiony');

    // W trybie deweloperskim z Strict Mode ten efekt
    // uruchomi się DWA RAZY, aby pomóc wykryć błędy

    let anulowany = false;

    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(data => {
        // Sprawdź czy komponent nie został "odmontowany"
        if (!anulowany) {
          console.log('Dane pobrane:', data);
        }
      });

    // Cleanup - oznacz jako anulowany
    return () => {
      console.log('Cleanup uruchomiony');
      anulowany = true;
    };
  }, []);

  return <div>Zobacz konsolę (Strict Mode)</div>;
}

Diagram cyklu życia z pustą tablicą:

sequenceDiagram
    participant C as Komponent
    participant R as React
    participant E as useEffect([])
    participant D as DOM

    Note over C,D: Montowanie
    C->>R: Pierwszy render
    R->>D: Aktualizacja DOM
    D->>R: DOM gotowy
    R->>E: Uruchom efekt (raz)
    E->>E: Wykonaj kod efektu
    E-->>R: Zwróć cleanup function

    Note over C,D: Aktualizacje (re-renders)
    C->>R: Re-render (zmiana state/props)
    R->>D: Aktualizacja DOM
    Note over E: ❌ Efekt NIE uruchamia się

    C->>R: Kolejny re-render
    R->>D: Aktualizacja DOM
    Note over E: ❌ Efekt NIE uruchamia się

    Note over C,D: Odmontowanie
    C->>R: Unmount
    R->>E: Uruchom cleanup
    E->>E: Wykonaj funkcję cleanup
    Note over C,D: Komponent usunięty

Porównanie z komponentami klasowymi:

// Komponent klasowy
class KlasownyKomponent extends React.Component {
  componentDidMount() {
    // Wykonuje się raz po montowaniu
    console.log('Mounted');
    fetch('/api/data').then(/* ... */);
  }

  componentWillUnmount() {
    // Wykonuje się przed odmontowaniem
    console.log('Unmounting');
  }

  render() {
    return <div>Zawartość</div>;
  }
}

// Odpowiednik z Hooks
function FunkcyjnyKomponent() {
  useEffect(() => {
    // componentDidMount
    console.log('Mounted');
    fetch('/api/data').then(/* ... */);

    return () => {
      // componentWillUnmount
      console.log('Unmounting');
    };
  }, []); // Pusta tablica = tylko mount/unmount

  return <div>Zawartość</div>;
}

Materiały:

React Hooks - Kategoria 4: useContext

Czym jest Hook useContext i jak zastępuje Context.Consumer?

Odpowiedź w 30 sekund: Hook useContext to nowoczesny sposób odczytywania wartości z React Context, który zastępuje starszą składnię Context.Consumer. Zamiast zagnieżdżonych komponentów Consumer, useContext pozwala na proste wywołanie hooka w ciele komponentu funkcyjnego, co znacząco poprawia czytelność kodu i eliminuje "callback hell".

Odpowiedź w 2 minuty: Hook useContext został wprowadzony w React 16.8 jako część Hooks API i stanowi bardziej elegancką alternatywę dla Context.Consumer. Przyjmuje obiekt kontekstu (zwrócony z React.createContext) jako argument i zwraca aktualną wartość kontekstu. Wartość ta jest określana przez prop value najbliższego Provider powyżej komponentu w drzewie.

Główne zalety useContext nad Context.Consumer to: lepsza czytelność kodu poprzez eliminację dodatkowego poziomu zagnieżdżenia, możliwość używania wartości kontekstu bezpośrednio w logice komponentu (nie tylko w JSX), oraz łatwiejsze łączenie wielu kontekstów bez tworzenia głęboko zagnieżdżonych struktur. Hook useContext automatycznie powoduje re-render komponentu, gdy wartość kontekstu się zmienia.

Istotne jest, że useContext nie zastępuje Provider - nadal musisz owijać drzewo komponentów w Provider, aby dostarczyć wartość kontekstu. Hook useContext zastępuje jedynie sposób konsumowania tej wartości. Jeśli nie ma odpowiedniego Provider w drzewie komponentów, useContext zwróci wartość domyślną przekazaną do createContext.

Warto pamiętać, że komponent używający useContext zawsze będzie się re-renderował, gdy zmieni się wartość kontekstu, niezależnie od tego, czy używa wszystkich wartości z tego kontekstu. To może mieć wpływ na wydajność w większych aplikacjach.

Przykład kodu:

// Stara składnia z Context.Consumer
import React from 'react';

const ThemeContext = React.createContext('light');

function ThemedButtonOld() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <button className={theme}>
          Przycisk w starym stylu
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

// Nowa składnia z useContext
import React, { useContext } from 'react';

function ThemedButtonNew() {
  const theme = useContext(ThemeContext);

  return (
    <button className={theme}>
      Przycisk w nowym stylu
    </button>
  );
}

// Porównanie wielu kontekstów
function MultiContextOld() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <LanguageContext.Consumer>
              {language => (
                <div>
                  {/* Głęboko zagnieżdżony kod */}
                  <p>Motyw: {theme}</p>
                  <p>Użytkownik: {user.name}</p>
                  <p>Język: {language}</p>
                </div>
              )}
            </LanguageContext.Consumer>
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}

function MultiContextNew() {
  const theme = useContext(ThemeContext);
  const user = useContext(UserContext);
  const language = useContext(LanguageContext);

  return (
    <div>
      {/* Czysty, płaski kod */}
      <p>Motyw: {theme}</p>
      <p>Użytkownik: {user.name}</p>
      <p>Język: {language}</p>
    </div>
  );
}

Materiały

Jak stworzyć i użyć kontekstu z useContext?

Odpowiedź w 30 sekund: Aby stworzyć i użyć kontekstu, najpierw utwórz go za pomocą React.createContext(), następnie owin drzewo komponentów w Provider dostarczając wartość przez prop value, a w końcu użyj hooka useContext w komponentach potomnych, aby odczytać tę wartość. To standardowy wzorzec składający się z trzech kroków: create → provide → consume.

Odpowiedź w 2 minuty: Tworzenie i używanie kontekstu z useContext składa się z kilku kroków. Po pierwsze, tworzysz kontekst używając React.createContext(), przekazując opcjonalną wartość domyślną, która będzie użyta, gdy komponent nie znajdzie pasującego Provider w drzewie. Dobrą praktyką jest eksportowanie zarówno kontekstu, jak i custom hooka do jego używania.

Po drugie, musisz owinąć część drzewa komponentów w Provider, przekazując wartość, którą chcesz udostępnić. Provider może być umieszczony na dowolnym poziomie w hierarchii - wszystkie komponenty potomne będą miały dostęp do wartości kontekstu. Wartość może być dowolnego typu: obiekt, tablica, funkcja, liczba, string, a nawet inny React Context.

Po trzecie, w komponentach potomnych używasz hooka useContext, przekazując mu referencję do kontekstu. Hook zwróci aktualną wartość kontekstu z najbliższego Provider powyżej w drzewie. Jeśli wartość w Provider się zmieni, wszystkie komponenty używające useContext automatycznie się re-renderują.

Częstym wzorcem jest tworzenie dedykowanego komponentu Provider i custom hooka, który łączy stan lokalny (useState, useReducer) z Context API. To pozwala na enkapsulację logiki zarządzania stanem i udostępnienie zarówno wartości, jak i funkcji do jej modyfikacji. Taki wzorzec jest szczególnie użyteczny w większych aplikacjach, gdzie chcemy separować logikę biznesową od komponentów prezentacyjnych.

Przykład kodu:

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

// Krok 1: Stwórz kontekst z wartością domyślną
const AuthContext = createContext(null);

// Krok 2: Stwórz Provider z logiką zarządzania stanem
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  const login = async (username, password) => {
    setIsLoading(true);
    try {
      // Symulacja wywołania API
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password })
      });
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('Błąd logowania:', error);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = () => {
    setUser(null);
  };

  // Wartość kontekstu - obiekt z danymi i metodami
  const value = {
    user,
    isLoading,
    login,
    logout,
    isAuthenticated: !!user
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Krok 3: Stwórz custom hook dla wygody
function useAuth() {
  const context = useContext(AuthContext);

  // Walidacja - sprawdź czy hook jest użyty w Provider
  if (context === null) {
    throw new Error('useAuth musi być użyty wewnątrz AuthProvider');
  }

  return context;
}

// Użycie w komponencie głównym
function App() {
  return (
    <AuthProvider>
      <Navigation />
      <MainContent />
    </AuthProvider>
  );
}

// Krok 4: Użyj kontekstu w komponentach potomnych
function Navigation() {
  const { user, logout, isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <LoginButton />;
  }

  return (
    <nav>
      <span>Witaj, {user.name}!</span>
      <button onClick={logout}>Wyloguj</button>
    </nav>
  );
}

function LoginButton() {
  const { login, isLoading } = useAuth();

  const handleLogin = () => {
    login('user@example.com', 'password123');
  };

  return (
    <button onClick={handleLogin} disabled={isLoading}>
      {isLoading ? 'Logowanie...' : 'Zaloguj'}
    </button>
  );
}

// Przykład z wieloma kontekstami
const ThemeContext = createContext('light');
const LanguageContext = createContext('pl');

function AppWithMultipleContexts() {
  const [theme, setTheme] = useState('light');
  const [language, setLanguage] = useState('pl');

  return (
    <ThemeContext.Provider value={theme}>
      <LanguageContext.Provider value={language}>
        <AuthProvider>
          <App />
        </AuthProvider>
      </LanguageContext.Provider>
    </ThemeContext.Provider>
  );
}

Materiały

Kiedy warto używać useContext zamiast prop drilling?

Odpowiedź w 30 sekund: Warto używać useContext, gdy przekazujesz dane przez wiele poziomów komponentów, które same tych danych nie używają (prop drilling), lub gdy dane są potrzebne w wielu niezwiązanych miejscach aplikacji. Context jest szczególnie przydatny dla globalnych danych jak motyw, język, dane użytkownika, czy konfiguracja aplikacji.

Odpowiedź w 2 minuty: Wybór między prop drilling a useContext zależy od specyfiki aplikacji i struktury komponentów. Prop drilling (przekazywanie props przez wiele poziomów) jest prostsze i bardziej eksplicytne dla płytkich hierarchii (2-3 poziomy), ale staje się uciążliwe i trudne w utrzymaniu, gdy props muszą przejść przez 4+ poziomy komponentów pośrednich, które same tych danych nie używają.

UseContext jest lepszym wyborem w kilku scenariuszach: gdy dane są potrzebne w wielu miejscach aplikacji (jak preferencje użytkownika, motyw, język interfejsu), gdy masz głęboką hierarchię komponentów i przekazywanie props staje się niewygodne, gdy chcesz uniknąć "przekazywania przez" komponenty pośrednie, które nie potrzebują tych danych, oraz gdy dane się rzadko zmieniają (częste zmiany mogą powodować nadmierne re-rendery).

Nie należy jednak traktować Context jako zastępstwa dla wszystkich props. Props są lepsze dla lokalnej komunikacji rodzic-dziecko, dla danych specyficznych dla danego komponentu, oraz gdy chcesz zachować komponent reużywalny i niezależny od globalnego stanu. Context może utrudnić reużywalność komponentów, ponieważ wiąże je z konkretną strukturą Provider.

Dobre praktyki sugerują używanie Context dla danych "globalnych" (theme, auth, i18n), które są potrzebne w wielu miejscach, ale używanie props lub composition patterns (children, render props) dla lokalnej komunikacji między komponentami. W większych aplikacjach warto rozważyć biblioteki zarządzania stanem (Redux, Zustand, Jotai) dla złożonej logiki biznesowej.

Przykład kodu:

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

// ❌ Problematyczny prop drilling
function AppWithPropDrilling() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Jan' });

  return (
    <Layout theme={theme} user={user}>
      <Sidebar theme={theme} user={user}>
        <Navigation theme={theme} user={user}>
          <UserMenu theme={theme} user={user} />
        </Navigation>
      </Sidebar>
    </Layout>
  );
}

// Layout, Sidebar, Navigation nie używają theme/user,
// tylko przekazują je dalej!

// ✅ Lepsze rozwiązanie z Context
const ThemeContext = createContext();
const UserContext = createContext();

function AppWithContext() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Jan' });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <Layout>
          <Sidebar>
            <Navigation>
              <UserMenu />
            </Navigation>
          </Sidebar>
        </Layout>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Komponenty pośrednie są czyste - nie przekazują props
function Layout({ children }) {
  return <div className="layout">{children}</div>;
}

function Sidebar({ children }) {
  return <aside className="sidebar">{children}</aside>;
}

function Navigation({ children }) {
  return <nav>{children}</nav>;
}

// Tylko UserMenu faktycznie używa kontekstu
function UserMenu() {
  const { theme, setTheme } = useContext(ThemeContext);
  const { user, setUser } = useContext(UserContext);

  return (
    <div className={`user-menu ${theme}`}>
      <span>Witaj, {user.name}!</span>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Przełącz motyw
      </button>
    </div>
  );
}

// Diagram pokazujący różnicę
graph TD
    subgraph "Prop Drilling - Dane przechodzą przez każdy poziom"
        A1[App theme user] --> B1[Layout theme user]
        B1 --> C1[Sidebar theme user]
        C1 --> D1[Navigation theme user]
        D1 --> E1[UserMenu używa theme user]
    end

    subgraph "Context - Dane dostępne bezpośrednio"
        A2[App Provider theme user] -.-> E2[UserMenu useContext]
        A2 --> B2[Layout]
        B2 --> C2[Sidebar]
        C2 --> D2[Navigation]
        D2 --> E2
    end

    style E1 fill:#ff9999
    style E2 fill:#99ff99
    style A2 fill:#99ccff
// Kiedy NIE używać Context - przykład nadużycia
// ❌ Źle - Context dla lokalnego stanu formularza
const FormContext = createContext();

function FormWithContext() {
  const [formData, setFormData] = useState({});

  return (
    <FormContext.Provider value={{ formData, setFormData }}>
      <FormField name="email" />
      <FormField name="password" />
      <SubmitButton />
    </FormContext.Provider>
  );
}

// ✅ Lepiej - Props lub dedykowana biblioteka formularzy
function FormWithProps() {
  const [formData, setFormData] = useState({});

  const handleChange = (name, value) => {
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  return (
    <form>
      <FormField
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <FormField
        name="password"
        value={formData.password}
        onChange={handleChange}
      />
      <SubmitButton formData={formData} />
    </form>
  );
}

// Przykład praktyczny - kiedy używać którego podejścia
// ✅ Context: Globalne dane potrzebne w wielu miejscach
const AppSettingsContext = createContext();

function AppSettings({ children }) {
  const [settings] = useState({
    theme: 'dark',
    language: 'pl',
    timezone: 'Europe/Warsaw',
    notifications: true
  });

  return (
    <AppSettingsContext.Provider value={settings}>
      {children}
    </AppSettingsContext.Provider>
  );
}

// ✅ Props: Lokalna komunikacja rodzic-dziecko
function ProductList({ products }) {
  return (
    <div>
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={(id) => console.log('Dodano:', id)}
        />
      ))}
    </div>
  );
}

// ✅ Composition: Elastyczność bez prop drilling
function Modal({ title, children, footer }) {
  return (
    <div className="modal">
      <div className="modal-header">{title}</div>
      <div className="modal-body">{children}</div>
      <div className="modal-footer">{footer}</div>
    </div>
  );
}

// Użycie composition
function ConfirmDialog() {
  return (
    <Modal
      title="Potwierdź operację"
      footer={
        <>
          <button>Anuluj</button>
          <button>Potwierdź</button>
        </>
      }
    >
      <p>Czy na pewno chcesz kontynuować?</p>
    </Modal>
  );
}

Materiały

Jakie są ograniczenia useContext w kontekście wydajności?

Odpowiedź w 30 sekund: Główne ograniczenie useContext to brak automatycznej optymalizacji re-renderów - każdy komponent używający kontekstu re-renderuje się przy każdej zmianie wartości kontekstu, nawet jeśli używa tylko małej części tej wartości. To może prowadzić do problemów wydajnościowych w dużych aplikacjach, gdzie wiele komponentów subskrybuje ten sam kontekst.

Odpowiedź w 2 minuty: UseContext ma kilka istotnych ograniczeń wydajnościowych, które wynikają z jego prostego modelu subskrypcji. Podstawowy problem polega na tym, że React nie śledzi, które części wartości kontekstu są faktycznie używane przez dany komponent. Gdy kontekst Provider otrzyma nową wartość (nawet jeśli referencja obiektu się zmieni, ale zawartość jest taka sama), wszystkie komponenty używające useContext dla tego kontekstu zostaną automatycznie re-renderowane, niezależnie od tego, czy faktycznie używane przez nie dane się zmieniły.

Problem pogarsza się, gdy w kontekście przechowujemy złożony obiekt z wieloma właściwościami lub funkcjami. Jeśli komponent A używa tylko pola "theme", a komponent B tylko pola "user", to zmiana wartości "theme" spowoduje re-render obu komponentów, mimo że komponent B nie powinien być dotknięty. To tzw. "over-rendering" problem, który może znacząco wpłynąć na wydajność w większych aplikacjach z dziesiątkami lub setkami komponentów subskrybujących ten sam kontekst.

Dodatkowo, częstym błędem jest tworzenie nowych referencji obiektów lub funkcji w każdym renderze Provider, co powoduje niepotrzebne re-rendery wszystkich konsumentów. Na przykład, jeśli wartość kontekstu jest tworzona jako literał obiektu bezpośrednio w JSX Provider, ten obiekt będzie tworzony na nowo przy każdym renderze, nawet jeśli jego zawartość się nie zmieniła.

Istnieje kilka strategii optymalizacji: podział dużego kontekstu na mniejsze, wyspecjalizowane konteksty (separation of concerns), użycie useMemo do memoizacji wartości kontekstu, użycie useCallback dla funkcji przekazywanych w kontekście, oraz w skrajnych przypadkach - rozważenie zewnętrznych bibliotek zarządzania stanem (Redux, Zustand, Jotai), które oferują bardziej zaawansowane mechanizmy subskrypcji i selektorów. Warto też pamiętać o React.memo dla komponentów konsumujących kontekst, choć samo memo nie pomoże, jeśli kontekst się zmienia.

Przykład kodu:

import React, { createContext, useContext, useState, useMemo, useCallback, memo } from 'react';

// ❌ Problem wydajnościowy - nowy obiekt przy każdym renderze
function BadPerformanceProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Jan' });
  const [settings, setSettings] = useState({});

  // Nowy obiekt tworzony przy każdym renderze!
  return (
    <AppContext.Provider value={{
      theme,
      setTheme,
      user,
      setUser,
      settings,
      setSettings,
      // Nowa funkcja przy każdym renderze!
      toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
    }}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Optymalizacja z useMemo i useCallback
const AppContext = createContext();

function OptimizedProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Jan' });
  const [settings, setSettings] = useState({});

  // Memoizacja funkcji - ta sama referencja między renderami
  const toggleTheme = useCallback(() => {
    setTheme(t => t === 'light' ? 'dark' : 'light');
  }, []);

  // Memoizacja całej wartości kontekstu
  const value = useMemo(() => ({
    theme,
    setTheme,
    user,
    setUser,
    settings,
    setSettings,
    toggleTheme
  }), [theme, user, settings, toggleTheme]);

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// ✅ Jeszcze lepiej - podział na osobne konteksty
const ThemeContext = createContext();
const UserContext = createContext();
const SettingsContext = createContext();

function SplitContextProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'Jan' });
  const [settings, setSettings] = useState({});

  const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
  const userValue = useMemo(() => ({ user, setUser }), [user]);
  const settingsValue = useMemo(() => ({ settings, setSettings }), [settings]);

  return (
    <ThemeContext.Provider value={themeValue}>
      <UserContext.Provider value={userValue}>
        <SettingsContext.Provider value={settingsValue}>
          {children}
        </SettingsContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// Teraz komponenty subskrybują tylko to, czego potrzebują
function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  // Ten komponent NIE re-renderuje się, gdy zmienia się user lub settings

  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Aktualny motyw: {theme}
    </button>
  );
}

function UserProfile() {
  const { user } = useContext(UserContext);
  // Ten komponent NIE re-renderuje się, gdy zmienia się theme lub settings

  return <div>Witaj, {user.name}!</div>;
}

// Monitoring re-renderów
function ComponentWithRenderTracking({ name, children }) {
  const renderCount = React.useRef(0);
  renderCount.current++;

  console.log(`${name} renderowany ${renderCount.current} razy`);

  return children;
}

// Demo problemu wydajnościowego
function PerformanceDemo() {
  return (
    <OptimizedProvider>
      <ComponentWithRenderTracking name="ThemeConsumer">
        <ThemeToggle />
      </ComponentWithRenderTracking>

      <ComponentWithRenderTracking name="UserConsumer">
        <UserProfile />
      </ComponentWithRenderTracking>

      <ComponentWithRenderTracking name="BothConsumer">
        <ComponentUsingBoth />
      </ComponentWithRenderTracking>
    </OptimizedProvider>
  );
}

// React.memo NIE pomoże, jeśli kontekst się zmienia
const MemoizedComponent = memo(function ExpensiveComponent() {
  const { theme } = useContext(AppContext);

  console.log('ExpensiveComponent renderowany');

  // Ciężkie obliczenia...
  const expensiveValue = computeExpensiveValue();

  return <div className={theme}>{expensiveValue}</div>;
});

// ✅ Rozwiązanie - Context Selector Pattern
function createContextSelector(Context) {
  return function useContextSelector(selector) {
    const value = useContext(Context);
    const selectedValue = selector(value);

    // W prawdziwej implementacji użylibyśmy
    // useSyncExternalStore dla lepszej wydajności
    return selectedValue;
  };
}

// Użycie selektora (uproszczona wersja)
function ComponentWithSelector() {
  // Ten komponent re-renderuje się TYLKO gdy zmienia się theme
  const theme = useContextSelector(AppContext, state => state.theme);

  return <div className={theme}>Tylko theme</div>;
}

// Przykład z biblioteką Zustand (lepsza alternatywa dla złożonego stanu)
import create from 'zustand';

const useStore = create((set) => ({
  theme: 'light',
  user: { name: 'Jan' },
  settings: {},

  setTheme: (theme) => set({ theme }),
  setUser: (user) => set({ user }),
  setSettings: (settings) => set({ settings }),

  toggleTheme: () => set((state) => ({
    theme: state.theme === 'light' ? 'dark' : 'light'
  }))
}));

// Zustand pozwala na selektory - komponent re-renderuje się
// tylko gdy wybrana wartość się zmienia
function ThemeToggleWithZustand() {
  const theme = useStore(state => state.theme);
  const toggleTheme = useStore(state => state.toggleTheme);

  return (
    <button onClick={toggleTheme}>
      Aktualny motyw: {theme}
    </button>
  );
}

// Wskazówki optymalizacyjne - checklist
const PerformanceChecklist = `
1. ✅ Używaj useMemo dla wartości kontekstu
2. ✅ Używaj useCallback dla funkcji w kontekście
3. ✅ Podziel duże konteksty na mniejsze
4. ✅ Umieść Provider jak najniżej w drzewie
5. ✅ Rozważ Context Selector lub biblioteki typu Zustand
6. ✅ Monitoruj re-rendery w DevTools Profiler
7. ❌ Nie twórz literałów obiektów bezpośrednio w Provider value
8. ❌ Nie używaj Context dla często zmieniających się danych
9. ❌ Nie umieszczaj całego stanu aplikacji w jednym kontekście
`;

// Diagram pokazujący problem re-renderów
graph TD
    subgraph "Problem: Jeden kontekst dla wszystkiego"
        P1[Provider value: theme user settings]
        P1 -->|zmiana theme| C1[Component A używa theme]
        P1 -->|zmiana theme| C2[Component B używa user]
        P1 -->|zmiana theme| C3[Component C używa settings]

        style C1 fill:#99ff99
        style C2 fill:#ff9999
        style C3 fill:#ff9999
    end

    subgraph "Rozwiązanie: Osobne konteksty"
        P2[ThemeProvider]
        P3[UserProvider]
        P4[SettingsProvider]

        P2 -->|zmiana theme| C4[Component A]
        P3 -.->|bez zmian| C5[Component B]
        P4 -.->|bez zmian| C6[Component C]

        style C4 fill:#99ff99
        style C5 fill:#cccccc
        style C6 fill:#cccccc
    end

    Note1[Czerwony = niepotrzebny re-render]
    Note2[Zielony = potrzebny re-render]
    Note3[Szary = brak re-renderu]

Materiały

React Hooks - Kategoria 5: useReducer

Kiedy warto użyć useReducer zamiast useState?

Odpowiedź w 30 sekund: useReducer jest lepszym wyborem gdy zarządzasz złożonym stanem z wieloma powiązanymi wartościami lub gdy następna wartość stanu zależy od poprzedniej. Szczególnie przydatny gdy logika aktualizacji stanu jest skomplikowana lub gdy chcesz oddzielić logikę stanu od komponentu.

Odpowiedź w 2 minuty: useState sprawdza się doskonale dla prostego, niezależnego stanu, ale useReducer staje się lepszym wyborem w kilku scenariuszach. Po pierwsze, gdy stan komponentu zawiera wiele powiązanych wartości (np. formularz z wieloma polami), useReducer pozwala zarządzać nimi w sposób bardziej zorganizowany. Po drugie, gdy aktualizacje stanu są złożone i wymagają skomplikowanej logiki - reducer centralizuje tę logikę w jednym miejscu zamiast rozpraszać ją po całym komponencie.

useReducer szczególnie dobrze sprawdza się gdy masz do czynienia z zaawansowanymi przepływami danych, gdzie jedna akcja użytkownika powoduje kilka zmian w stanie. Ułatwia też testowanie - możesz testować logikę reducera niezależnie od komponentu. Dodatkowo, gdy stan staje się na tyle złożony, że rozważasz użycie biblioteki do zarządzania stanem (jak Redux), useReducer może być prostszą alternatywą.

Warto również rozważyć useReducer gdy funkcje aktualizujące stan są przekazywane głęboko w dół drzewa komponentów - możesz przekazać dispatch zamiast wielu różnych funkcji callback. Wreszcie, jeśli przechodzisz z kodu opartego na klasach z this.setState, useReducer będzie bardziej naturalnym odpowiednikiem.

Przykład kodu:

// ŹLE: useState dla złożonego stanu formularza
function FormularzUzytkownika() {
  const [imie, setImie] = useState('');
  const [nazwisko, setNazwisko] = useState('');
  const [email, setEmail] = useState('');
  const [wiek, setWiek] = useState(0);
  const [bledy, setBledy] = useState({});
  const [wysylanie, setWysylanie] = useState(false);

  const waliduj = () => {
    const noweBledy = {};
    if (!imie) noweBledy.imie = 'Imię jest wymagane';
    if (!email.includes('@')) noweBledy.email = 'Nieprawidłowy email';
    setBledy(noweBledy);
    return Object.keys(noweBledy).length === 0;
  };

  const wyslij = async () => {
    if (!waliduj()) return;
    setWysylanie(true);
    // ... logika wysyłania
    setWysylanie(false);
  };

  return (/* ... */);
}

// DOBRZE: useReducer dla złożonego stanu
const initialState = {
  dane: { imie: '', nazwisko: '', email: '', wiek: 0 },
  bledy: {},
  wysylanie: false,
  wyslano: false
};

function formReducer(state, action) {
  switch (action.type) {
    case 'ZMIEN_POLE':
      return {
        ...state,
        dane: { ...state.dane, [action.pole]: action.wartosc },
        // Wyczyść błąd dla tego pola
        bledy: { ...state.bledy, [action.pole]: null }
      };

    case 'USTAW_BLEDY':
      return { ...state, bledy: action.bledy };

    case 'ROZPOCZNIJ_WYSYLANIE':
      return { ...state, wysylanie: true, bledy: {} };

    case 'SUKCES_WYSYLANIA':
      return { ...state, wysylanie: false, wyslano: true };

    case 'BLAD_WYSYLANIA':
      return { ...state, wysylanie: false, bledy: action.bledy };

    case 'RESETUJ':
      return initialState;

    default:
      return state;
  }
}

function FormularzUzytkownika() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  const waliduj = () => {
    const bledy = {};
    if (!state.dane.imie) bledy.imie = 'Imię jest wymagane';
    if (!state.dane.email.includes('@')) bledy.email = 'Nieprawidłowy email';

    if (Object.keys(bledy).length > 0) {
      dispatch({ type: 'USTAW_BLEDY', bledy });
      return false;
    }
    return true;
  };

  const wyslij = async () => {
    if (!waliduj()) return;

    dispatch({ type: 'ROZPOCZNIJ_WYSYLANIE' });

    try {
      await api.wyslij(state.dane);
      dispatch({ type: 'SUKCES_WYSYLANIA' });
    } catch (error) {
      dispatch({ type: 'BLAD_WYSYLANIA', bledy: { ogolny: error.message } });
    }
  };

  return (/* ... */);
}

// Porównanie: Kiedy użyć którego?
//
// useState - użyj gdy:
// ✓ Stan jest prosty (string, number, boolean)
// ✓ Brak złożonej logiki aktualizacji
// ✓ Niezależne wartości stanu
// ✓ Proste operacje toggle lub increment
//
// useReducer - użyj gdy:
// ✓ Złożony obiekt stanu z wieloma polami
// ✓ Skomplikowana logika aktualizacji
// ✓ Wiele powiązanych wartości stanu
// ✓ Następny stan zależy od poprzedniego
// ✓ Potrzebujesz testować logikę stanu osobno
// ✓ Chcesz uniknąć wielu funkcji setState
Kryteria useState useReducer
Złożoność stanu Prosty stan Złożony, zagnieżdżony obiekt
Logika aktualizacji Prosta Skomplikowana
Liczba wartości 1-2 wartości Wiele powiązanych wartości
Testowanie Trudniejsze Łatwiejsze (reducer oddzielnie)
Czytelność Lepsza dla prostych przypadków Lepsza dla złożonych przypadków
Optymalizacja Łatwiej memorizować funkcje dispatch jest stabilny

Materiały:

21. Jak działa Hook useReducer i jakie przyjmuje parametry?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. Jaka jest różnica między useState a useReducer?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
23. Jak zaimplementować wzorzec reducer z akcjami typowanymi?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Kategoria 6: useMemo i useCallback

24. Do czego służy Hook useMemo i kiedy go używać?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
25. Jaka jest różnica między useMemo a useCallback?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
26. Kiedy NIE warto używać useMemo lub useCallback?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. Jak useMemo wpływa na wydajność renderowania?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
28. Jak prawidłowo memoizować komponenty potomne z useCallback?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

React Hooks - Kategoria 7: useRef

29. Do czego służy Hook useRef i czym różni się od useState?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. Jak użyć useRef do dostępu do elementu DOM?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
31. Dlaczego zmiana wartości ref nie powoduje ponownego renderowania?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
32. Jak przechowywać poprzednią wartość stanu za pomocą useRef?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Kategoria 8: Custom Hooks

33. Czym są Custom Hooks i jak je tworzyć?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
34. Jakie są konwencje nazewnictwa dla Custom Hooks?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
35. Jak współdzielić logikę między komponentami za pomocą Custom Hooks?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
36. Podaj przykład praktycznego zastosowania Custom Hook (np. useFetch, useLocalStorage)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Sekcja 9: Reguły Hooków

37. Jakie są dwie główne reguły używania Hooków (Rules of Hooks)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Dlaczego Hooki muszą być wywoływane na najwyższym poziomie komponentu?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
39. Jak ESLint plugin react-hooks pomaga w przestrzeganiu reguł?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
40. Co się stanie, jeśli wywołamy Hook warunkowo?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

React 18+ Concurrent Hooks

41. Jak działa useTransition i kiedy go używać do poprawy UX?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
42. Jak używać useDeferredValue do optymalizacji renderowania?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
43. Do czego służy useId i jak rozwiązuje problemy z hydracją SSR?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
44. Jak używać useSyncExternalStore do integracji z zewnętrznymi źródłami danych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
45. Jak działa hook use() w React 19 i jak zmienia obsługę asynchroniczności?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
46. Jak używać useOptimistic do natychmiastowej aktualizacji UI?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
47. Jak działa useActionState (dawniej useFormState) w React 19?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
48. Jak tworzyć zaawansowane Custom Hooks z pełnym TypeScript i testowalnością?

Odpowiedź dostępna w pełnej wersji. Odblokuj 28 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 48 pytań rekrutacyjnych z React Hooks oraz pozostałych technologii.

Zobacz plany cenowe