To jest darmowy podgląd

To jest próbka 15 pytań z naszej pełnej kolekcji 40 pytań rekrutacyjnych. Uzyskaj pełny dostęp do wszystkich pytań ze szczegółowymi odpowiedziami i przykładami kodu.

Kup pełny dostęp

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

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:

↑ Powrót na górę

React Hooks - Kategoria 3: useEffect

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:

↑ Powrót na górę

React Hooks - Kategoria 5: useReducer

Jak działa Hook useReducer i jakie przyjmuje parametry?

Odpowiedź w 30 sekund: useReducer przyjmuje trzy parametry: funkcję reducer (stan, akcja) => nowy stan, stan początkowy oraz opcjonalną funkcję inicjalizującą. Zwraca tablicę z aktualnym stanem i funkcją dispatch do wysyłania akcji. Każde wywołanie dispatch uruchamia reducer, który oblicza nowy stan na podstawie obecnego stanu i przekazanej akcji.

Odpowiedź w 2 minuty: useReducer to Hook, który działa na podobnej zasadzie jak reducery w Redux. Przyjmuje on funkcję reducer jako pierwszy parametr - ta funkcja musi być czystą funkcją przyjmującą dwa argumenty: aktualny stan (state) i obiekt akcji (action), a zwracającą nowy stan. Drugi parametr to wartość początkowa stanu (initialState). Trzeci, opcjonalny parametr to funkcja init, która pozwala na leniwą inicjalizację stanu.

Hook zwraca tablicę z dwoma elementami: aktualnym stanem oraz funkcją dispatch. Funkcja dispatch służy do wysyłania akcji - gdy ją wywołasz, React uruchomi funkcję reducer z aktualnym stanem i przekazaną akcją, a następnie zaktualizuje stan do wartości zwróconej przez reducer. Ważne jest, że funkcja dispatch ma stabilną identyfikację między renderowaniami, co czyni ją bezpieczną w zależnościach useEffect i useCallback.

Funkcja reducer powinna być zawsze czysta - dla tych samych argumentów musi zwracać ten sam rezultat, bez efektów ubocznych. Akcje zazwyczaj mają strukturę z polem 'type' określającym typ akcji oraz opcjonalnymi dodatkowymi polami z danymi (payload). React wywołuje reducer podczas renderowania, więc musi on być synchroniczny i nie może wykonywać operacji asynchronicznych.

Opcjonalna funkcja init jako trzeci parametr pozwala przenieść logikę obliczania początkowego stanu poza komponent. Jest wywoływana tylko raz podczas inicjalizacji z initialState jako argumentem, co jest przydatne dla kosztownych obliczeń lub gdy chcesz mieć funkcję resetującą stan do wartości początkowej.

Przykład kodu:

// Podstawowe użycie useReducer
import { useReducer } from 'react';

// 1. Definiujemy funkcję reducer
// Parametry: state (aktualny stan), action (obiekt akcji)
// Zwraca: nowy stan
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'ADD':
      return { count: state.count + action.value };
    case 'RESET':
      return { count: 0 };
    default:
      throw new Error(`Nieznana akcja: ${action.type}`);
  }
}

function Licznik() {
  // 2. Stan początkowy
  const initialState = { count: 0 };

  // 3. useReducer zwraca [state, dispatch]
  const [state, dispatch] = useReducer(counterReducer, initialState);

  // 4. Wysyłanie akcji za pomocą dispatch
  return (
    <div>
      <p>Licznik: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'ADD', value: 5 })}>+5</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

// Przykład z leniwą inicjalizacją (trzeci parametr)
function complexInitializer(initialCount) {
  // Ta funkcja jest wywoływana tylko raz przy montowaniu
  console.log('Inicjalizacja stanu...');
  return {
    count: initialCount,
    history: [initialCount],
    maxValue: initialCount
  };
}

function counterWithHistoryReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      const newCount = state.count + 1;
      return {
        count: newCount,
        history: [...state.history, newCount],
        maxValue: Math.max(state.maxValue, newCount)
      };
    case 'RESET':
      // Możemy użyć funkcji init do resetu
      return complexInitializer(action.initialCount);
    default:
      return state;
  }
}

function LicznikZHistoria() {
  const [state, dispatch] = useReducer(
    counterWithHistoryReducer,
    10, // initialArg - przekazany do init
    complexInitializer // init - funkcja inicjalizująca
  );

  return (
    <div>
      <p>Licznik: {state.count}</p>
      <p>Maksymalna wartość: {state.maxValue}</p>
      <p>Historia: {state.history.join(', ')}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'RESET', initialCount: 0 })}>
        Reset do 0
      </button>
    </div>
  );
}

// Przykład pokazujący wszystkie parametry
function DemoParametrow() {
  // Parametr 1: reducer function
  const reducer = (state, action) => {
    // Musi być czysta funkcja
    // Przyjmuje: aktualny stan i akcję
    // Zwraca: nowy stan
    return { ...state };
  };

  // Parametr 2: initial state
  const initialState = { value: 0 };

  // Parametr 3: init function (opcjonalna)
  const init = (initialArg) => {
    // Wywoływana tylko raz z initialArg
    return { value: initialArg * 2 };
  };

  // Bez funkcji init:
  const [state1, dispatch1] = useReducer(reducer, initialState);

  // Z funkcją init:
  const [state2, dispatch2] = useReducer(reducer, 5, init);
  // state2 będzie { value: 10 }, bo init(5) zwraca { value: 10 }

  // dispatch ma stabilną referencję - bezpieczny w zależnościach
  useEffect(() => {
    // dispatch nie zmienia się między renderowaniami
    dispatch1({ type: 'SOME_ACTION' });
  }, [dispatch1]); // Bezpieczne - dispatch jest stabilny

  return null;
}

Materiały:

↑ Powrót na górę

React Hooks - Kategoria 7: useRef

Dlaczego zmiana wartości ref nie powoduje ponownego renderowania?

Odpowiedź w 30 sekund: Zmiana wartości ref nie powoduje ponownego renderowania, ponieważ React nie śledzi mutacji właściwości current obiektu ref. W przeciwieństwie do useState, który jest zintegrowany z mechanizmem renderowania React, useRef zwraca zwykły obiekt JavaScript, którego modyfikacje są całkowicie poza kontrolą React - nie ma mechanizmu subskrypcji ani obserwatorów, które powiadomiłyby React o zmianie wartości.

Odpowiedź w 2 minuty: Fundamentalna różnica między useRef a useState wynika z ich architektury wewnętrznej w React. Kiedy wywołujesz useState, React tworzy wpis w wewnętrznej kolejce stanów komponentu i ustanawia mechanizm śledzenia zmian. Każde wywołanie funkcji setState jest rejestrowane, co powoduje zaplanowanie ponownego renderowania komponentu. React zarządza tym procesem, porównuje stare i nowe wartości, i decyduje, kiedy i jak zaktualizować interfejs użytkownika.

useRef działa zupełnie inaczej. Hook ten po prostu zwraca zwykły obiekt JavaScript w postaci { current: wartośćPoczątkowa }. Ten obiekt jest tworzony przy pierwszym renderowaniu i ten sam obiekt (ta sama referencja w pamięci) jest zwracany przy każdym kolejnym renderowaniu. Ponieważ jest to zwykły obiekt JavaScript, możesz swobodnie mutować jego właściwość current bez angażowania jakiegokolwiek mechanizmu React. React po prostu nie wie o tych zmianach i nie ma powodu, aby reagować na nie.

To projektowa decyzja, która zapewnia escape hatch - sposób na przechowywanie wartości mutowalnych poza systemem reaktywności React. Gdyby useRef powodował re-rendering przy każdej zmianie, nie różniłby się od useState i nie spełniałby swojego głównego celu. Dzięki temu, że zmiana ref jest "niewidzialna" dla React, możemy używać go do przechowywania wartości pomocniczych (jak ID timerów, liczniki, poprzednie wartości) bez powodowania niepotrzebnych re-renderów, co poprawia wydajność.

Mechanizm ten jest szczególnie przydatny, gdy potrzebujesz zachować wartość między renderowaniami, ale ta wartość nie wpływa na to, co jest renderowane w JSX. Jeśli zmieniasz ref.current i jednocześnie potrzebujesz zaktualizować UI, musisz osobno wywołać setState lub użyć innego mechanizmu wymuszającego re-render. React nie robi tego automatycznie, co daje programiście pełną kontrolę nad tym, kiedy komponent powinien się ponownie renderować.

Przykład kodu:

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

// Demonstracja: ref nie wywołuje re-render
function RenderBehaviorDemo() {
  const [count, setCount] = useState(0);
  const refCount = useRef(0);
  const renderCountRef = useRef(0);

  // Liczy każde renderowanie
  useEffect(() => {
    renderCountRef.current += 1;
    console.log('Komponent został wyrenderowany');
  });

  const incrementState = () => {
    setCount(c => c + 1);
    console.log('State zmieniony - wywoła re-render');
  };

  const incrementRef = () => {
    refCount.current += 1;
    console.log(`Ref zmieniony na ${refCount.current} - NIE wywoła re-render`);
    // Zauważ: wartość w UI nie zostanie zaktualizowana!
  };

  const incrementBoth = () => {
    refCount.current += 1;
    setCount(c => c + 1);
    // State wywoła re-render, więc zaktualizowane ref też będzie widoczne
  };

  return (
    <div>
      <h3>Analiza renderowania</h3>
      <p>Liczba renderowań: {renderCountRef.current}</p>
      <p>State count: {count}</p>
      <p>Ref count: {refCount.current}</p>

      <button onClick={incrementState}>
        Zwiększ State (wywoła re-render)
      </button>
      <button onClick={incrementRef}>
        Zwiększ Ref (NIE wywoła re-render)
      </button>
      <button onClick={incrementBoth}>
        Zwiększ oba (wywoła re-render)
      </button>
    </div>
  );
}

// Praktyczny przykład: śledzenie kliknięć bez re-renderów
function ClickTracker() {
  const clickCountRef = useRef(0);
  const lastClickTimeRef = useRef(null);

  const handleClick = () => {
    clickCountRef.current += 1;
    lastClickTimeRef.current = new Date();

    // Logujemy informacje, ale nie aktualizujemy UI
    console.log(`Kliknięcie #${clickCountRef.current}`);
    console.log(`Czas: ${lastClickTimeRef.current.toLocaleTimeString()}`);

    // UI nie zostanie zaktualizowany, co jest zamierzone
    // - nie chcemy re-renderować przy każdym kliknięciu
  };

  const showStats = () => {
    // Dopiero teraz pokazujemy statystyki (możesz użyć alert lub state)
    alert(`
      Total kliknięć: ${clickCountRef.current}
      Ostatnie kliknięcie: ${lastClickTimeRef.current?.toLocaleTimeString() || 'brak'}
    `);
  };

  return (
    <div>
      <button onClick={handleClick}>
        Kliknij mnie (sprawdź console)
      </button>
      <button onClick={showStats}>
        Pokaż statystyki
      </button>
    </div>
  );
}

// Porównanie wydajności: timer z ref vs state
function TimerComparison() {
  const [stateTime, setStateTime] = useState(0);
  const refTime = useRef(0);
  const intervalRef = useRef(null);
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
  });

  // Timer ze state - powoduje re-render co sekundę
  const startStateTimer = () => {
    intervalRef.current = setInterval(() => {
      setStateTime(t => t + 1);
      // Wywołuje re-render co sekundę - może być kosztowne
    }, 1000);
  };

  // Timer z ref - NIE powoduje re-render
  const startRefTimer = () => {
    intervalRef.current = setInterval(() => {
      refTime.current += 1;
      console.log('Ref time:', refTime.current);
      // NIE wywołuje re-render - wydajniejsze dla wartości pomocniczych
    }, 1000);
  };

  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
  };

  const showRefTime = () => {
    alert(`Ref time: ${refTime.current}s`);
  };

  return (
    <div>
      <p>Liczba renderowań: {renderCount.current}</p>
      <div>
        <h4>Timer z State (renderuje co sekundę)</h4>
        <p>Czas: {stateTime}s</p>
        <button onClick={startStateTimer}>Start State Timer</button>
      </div>
      <div>
        <h4>Timer z Ref (nie renderuje)</h4>
        <p>Sprawdź console lub kliknij "Pokaż czas"</p>
        <button onClick={startRefTimer}>Start Ref Timer</button>
        <button onClick={showRefTime}>Pokaż czas</button>
      </div>
      <button onClick={stopTimer}>Stop Timer</button>
    </div>
  );
}

// Wyjaśnienie wewnętrzne: uproszczona implementacja
/*
Uproszczona wersja pokazująca różnicę w implementacji:

// useState - śledzi zmiany i planuje re-render
function useState(initialValue) {
  const state = { value: initialValue };

  const setState = (newValue) => {
    state.value = newValue;
    scheduleRerender(); // ← KLUCZOWA różnica
  };

  return [state.value, setState];
}

// useRef - tylko zwraca obiekt, bez śledzenia
function useRef(initialValue) {
  const ref = { current: initialValue };
  // Brak mechanizmu śledzenia zmian!
  // Brak wywołania scheduleRerender()
  return ref;
}
*/

Materiały:

↑ Powrót na górę

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

↑ Powrót na górę

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

↑ Powrót na górę

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

↑ Powrót na górę

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

↑ Powrót na górę

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

↑ Powrót na górę

Kategoria 6: useMemo i useCallback

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

Odpowiedź w 30 sekund: Hook useMemo służy do memoizacji wyniku kosztownych obliczeń, aby uniknąć ich ponownego wykonywania przy każdym renderowaniu komponentu. Używamy go, gdy mamy złożone obliczenia lub transformacje danych, które powinny być ponownie wykonane tylko wtedy, gdy zmienią się określone zależności.

Odpowiedź w 2 minuty: useMemo to hook optymalizacyjny, który zapamiętuje (cache'uje) wynik funkcji i zwraca go przy kolejnych renderowaniach, dopóki nie zmienią się zależności podane w tablicy dependencies. React porównuje zależności używając Object.is() i tylko gdy któraś z nich się zmieni, funkcja obliczeniowa zostanie ponownie wykonana.

Główne przypadki użycia to: kosztowne obliczenia matematyczne (np. obliczanie statystyk z dużych zbiorów danych), złożone filtrowanie i sortowanie list, formatowanie danych, oraz tworzenie obiektów lub tablic, które są przekazywane jako propsy do zmemoizowanych komponentów potomnych. Bez useMemo takie operacje byłyby wykonywane przy każdym renderowaniu, nawet gdy dane wejściowe się nie zmieniły.

Ważne jest, aby używać useMemo rozsądnie - nie każda operacja wymaga memoizacji. Hook ten wprowadza niewielki narzut związany z porównywaniem zależności, więc powinien być stosowany tylko wtedy, gdy koszt ponownego wykonania obliczeń jest wyższy niż koszt samej memoizacji.

Typowym przykładem jest przetwarzanie dużych list danych, gdzie filtrowanie i sortowanie może być kosztowne. Zamiast wykonywać te operacje przy każdym renderowaniu (które może być wywołane przez zmiany w całkowicie niezwiązanym stanie), useMemo pozwala na wykonanie ich tylko wtedy, gdy zmienią się dane źródłowe lub kryteria filtrowania.

Przykład kodu:

import { useMemo, useState } from 'react';

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');

  // Kosztowne obliczenie - filtrowanie i sortowanie
  const processedProducts = useMemo(() => {
    console.log('Przetwarzanie produktów...');

    // Filtrowanie
    let filtered = products.filter(product =>
      product.name.toLowerCase().includes(filter.toLowerCase())
    );

    // Sortowanie
    filtered.sort((a, b) => {
      if (sortOrder === 'asc') {
        return a.price - b.price;
      }
      return b.price - a.price;
    });

    return filtered;
  }, [products, filter, sortOrder]); // Przelicz tylko gdy zmienią się te wartości

  // Kosztowne obliczenie statystyk
  const statistics = useMemo(() => {
    console.log('Obliczanie statystyk...');

    return {
      total: processedProducts.length,
      avgPrice: processedProducts.reduce((sum, p) => sum + p.price, 0) / processedProducts.length,
      maxPrice: Math.max(...processedProducts.map(p => p.price)),
      minPrice: Math.min(...processedProducts.map(p => p.price))
    };
  }, [processedProducts]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Szukaj produktu..."
      />
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        Sortuj: {sortOrder === 'asc' ? 'Rosnąco' : 'Malejąco'}
      </button>

      <div>
        <p>Znaleziono: {statistics.total}</p>
        <p>Średnia cena: {statistics.avgPrice.toFixed(2)} zł</p>
        <p>Zakres: {statistics.minPrice} - {statistics.maxPrice} zł</p>
      </div>

      <ul>
        {processedProducts.map(product => (
          <li key={product.id}>{product.name} - {product.price} zł</li>
        ))}
      </ul>
    </div>
  );
}

// Przykład: kiedy NIE używać useMemo
function Counter() {
  const [count, setCount] = useState(0);

  // ❌ ZŁE - niepotrzebne użycie useMemo dla prostej operacji
  const doubledCount = useMemo(() => count * 2, [count]);

  // ✅ DOBRE - prosta operacja nie wymaga memoizacji
  const tripleCount = count * 3;

  return <div>{doubledCount} / {tripleCount}</div>;
}

Materiały:

↑ Powrót na górę

Kategoria 8: Custom Hooks

Czym są Custom Hooks i jak je tworzyć?

Odpowiedź w 30 sekund: Custom Hooks to funkcje JavaScript, których nazwy zaczynają się od "use" i które mogą wywoływać inne Hooks. Pozwalają na wyodrębnienie logiki komponentu do funkcji wielokrotnego użytku, umożliwiając dzielenie się logiką stanową między komponentami bez konieczności modyfikacji hierarchii komponentów.

Odpowiedź w 2 minuty: Custom Hooks to mechanizm w React, który pozwala na ekstrakcję i ponowne wykorzystanie logiki komponentowej w sposób zgodny z zasadami Hooks. W przeciwieństwie do komponentów wyższego rzędu (HOC) czy render props, Custom Hooks nie dodają dodatkowych elementów do drzewa komponentów, co czyni kod czystszym i łatwiejszym w debugowaniu.

Tworzenie Custom Hook polega na utworzeniu funkcji, której nazwa zaczyna się od "use", co jest konwencją wymaganą przez React. Wewnątrz tej funkcji możesz używać wbudowanych Hooks takich jak useState, useEffect, useContext, a także innych Custom Hooks. Custom Hook może przyjmować argumenty i zwracać dowolne wartości - najczęściej obiekty, tablice lub pojedyncze wartości.

Kluczową zaletą Custom Hooks jest to, że każde wywołanie Hooka jest całkowicie niezależne - każdy komponent używający tego samego Custom Hooka ma własny, izolowany stan. Dzięki temu możemy bezpiecznie używać tego samego Custom Hooka w wielu miejscach bez obawy o konflikty stanu.

Custom Hooks świetnie nadają się do enkapsulacji złożonej logiki, integracji z API, zarządzania formularzami, obsługi subskrypcji, timera, local storage i wielu innych powtarzalnych wzorców w aplikacjach React.

Przykład kodu:

// Przykład prostego Custom Hook do zarządzania licznikiem
function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount(prev => prev + step);
  }, [step]);

  const decrement = useCallback(() => {
    setCount(prev => prev - step);
  }, [step]);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

// Użycie w komponencie
function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter(0, 5);

  return (
    <div>
      <p>Licznik: {count}</p>
      <button onClick={increment}>+5</button>
      <button onClick={decrement}>-5</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

// Zaawansowany przykład - Hook do zarządzania formularzem
function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const handleChange = useCallback((event) => {
    const { name, value } = event.target;
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
    // Wyczyść błąd dla tego pola
    setErrors(prev => ({
      ...prev,
      [name]: undefined
    }));
  }, []);

  const handleSubmit = useCallback((onSubmit, validate) => {
    return (event) => {
      event.preventDefault();

      if (validate) {
        const validationErrors = validate(values);
        if (Object.keys(validationErrors).length > 0) {
          setErrors(validationErrors);
          return;
        }
      }

      onSubmit(values);
    };
  }, [values]);

  const reset = useCallback(() => {
    setValues(initialValues);
    setErrors({});
  }, [initialValues]);

  return {
    values,
    errors,
    handleChange,
    handleSubmit,
    reset
  };
}

Materiały:

↑ Powrót na górę

Sekcja 9: Reguły Hooków

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

Odpowiedź w 30 sekund: Dwie główne reguły Hooków to: (1) Wywołuj Hooki tylko na najwyższym poziomie - nigdy wewnątrz pętli, warunków lub zagnieżdżonych funkcji, oraz (2) Wywołuj Hooki tylko z komponentów funkcyjnych React lub własnych Hooków - nigdy z zwykłych funkcji JavaScript. Te reguły zapewniają, że Hooki są wywoływane w tej samej kolejności przy każdym renderowaniu.

Odpowiedź w 2 minuty: React definiuje dwie fundamentalne reguły używania Hooków, które są krytyczne dla ich poprawnego działania. Pierwsza reguła mówi, że Hooki muszą być wywoływane tylko na najwyższym poziomie komponentu, przed jakimkolwiek wczesnym returnem. Oznacza to, że nie możesz wywoływać Hooków wewnątrz pętli, instrukcji warunkowych, lub zagnieżdżonych funkcji. Ta reguła zapewnia, że Hooki są wywoływane w tej samej kolejności przy każdym renderowaniu komponentu.

Druga reguła określa, że Hooki mogą być wywoływane tylko z dwóch miejsc: komponentów funkcyjnych React lub własnych Hooków (custom hooks). Nie możesz wywoływać Hooków z regularnych funkcji JavaScript, klas komponentów, lub funkcji obsługi zdarzeń. To ograniczenie pozwala React śledzić stan związany z konkretnymi komponentami i zapewnia, że logika Hooków jest izolowana w ekosystemie React.

Te reguły nie są arbitralne - wynikają z wewnętrznej implementacji Hooków w React. React polega na kolejności wywołań Hooków, aby powiązać wartości stanu i efektów między kolejnymi renderowaniami. Naruszenie tych reguł prowadzi do bugów, gdzie Hooki mogą otrzymywać dane z niewłaściwych źródeł lub całkowicie tracić stan.

Aby pomóc w przestrzeganiu tych reguł, zespół React stworzył plugin ESLint o nazwie eslint-plugin-react-hooks, który automatycznie wykrywa naruszenia i ostrzega deweloperów podczas pisania kodu. Plugin ten jest silnie zalecany w każdym projekcie używającym Hooków.

Przykład kodu:

// ✅ POPRAWNIE - Hooki na najwyższym poziomie
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(data => {
      setUser(data);
      setLoading(false);
    });
  }, [userId]);

  if (loading) return <div>Ładowanie...</div>;
  return <div>{user.name}</div>;
}

// ❌ BŁĄD - Hook wewnątrz warunku
function UserProfile({ userId }) {
  const [loading, setLoading] = useState(true);

  if (userId) {
    // BŁĄD: Hook wywołany warunkowo!
    const [user, setUser] = useState(null);
  }

  return <div>Profil</div>;
}

// ❌ BŁĄD - Hook w pętli
function UserList({ userIds }) {
  const users = [];

  for (let id of userIds) {
    // BŁĄD: Hook w pętli!
    const [user] = useState(null);
    users.push(user);
  }

  return <div>{users.length}</div>;
}

// ❌ BŁĄD - Hook w zwykłej funkcji
function calculateTotal() {
  // BŁĄD: Hook poza komponentem React!
  const [total, setTotal] = useState(0);
  return total;
}

// ✅ POPRAWNIE - Hook w custom hooku
function useCalculateTotal() {
  const [total, setTotal] = useState(0);

  const addToTotal = (value) => {
    setTotal(prev => prev + value);
  };

  return { total, addToTotal };
}

// ✅ POPRAWNIE - Używanie custom hooka w komponencie
function ShoppingCart() {
  const { total, addToTotal } = useCalculateTotal();

  return (
    <div>
      <p>Suma: {total} zł</p>
      <button onClick={() => addToTotal(10)}>Dodaj 10 zł</button>
    </div>
  );
}

Materiały:

↑ Powrót na górę

Chcesz więcej pytań?

Uzyskaj dostęp do 800+ pytań z 13 technologii - JavaScript, React, TypeScript, Node.js, SQL i więcej. Natychmiastowy dostęp na 30 dni.

Kup pełny dostęp za 49,99 zł