Lekcja 5: Stan (State) - Hook useState

W poprzedniej lekcji nauczyliśmy się przekazywać dane do komponentów za pomocą propsów. Propsy są jednak tylko do odczytu. Co jeśli komponent potrzebuje przechowywać dane, które mogą się zmieniać w czasie w odpowiedzi na interakcje użytkownika, dane z API czy upływ czasu? Do tego właśnie służy stan (state).

Czym jest Stan (State)?

Stan to obiekt danych, który należy do komponentu i może się zmieniać w trakcie jego życia. Kiedy stan komponentu się zmienia, React automatycznie ponownie renderuje ten komponent (i jego dzieci), aby odzwierciedlić te zmiany w interfejsie użytkownika.

Kluczowa różnica między stanem a propsami:

Hook `useState`

W komponentach funkcyjnych, stan dodajemy za pomocą Hooka useState. Hooki to specjalne funkcje (zaczynające się od "use"), które pozwalają "zahaczyć" o funkcje Reacta, takie jak zarządzanie stanem czy cykl życia, bez pisania komponentów klasowych.

Składnia `useState`:**

import React, { useState } from \'react\';

function MyComponent() {
  const [stateVariable, setStateFunction] = useState(initialValue);
  // ... reszta komponentu
}

Przykład: Prosty Licznik

Stwórzmy komponent licznika, który zwiększa wartość po kliknięciu przycisku.

import React, { useState } from \'react\';

function Counter() {
  // Inicjalizujemy stan \'count\' wartością początkową 0
  const [count, setCount] = useState(0);

  // Funkcja obsługująca kliknięcie przycisku
  const increment = () => {
    // Aktualizujemy stan, wywołując funkcję \'setCount\'
    setCount(count + 1);
  };

  return (
    <div>
      <p>Aktualna wartość licznika: {count}</p>
      <button onClick={increment}>Zwiększ licznik</button>
    </div>
  );
}

export default Counter;

Jak to działa?

  1. Podczas pierwszego renderowania, useState(0) zwraca [0, funkcja]. count ma wartość 0.
  2. Wyświetlamy count (czyli 0) w paragrafie.
  3. Gdy użytkownik klika przycisk, wywoływana jest funkcja increment.
  4. increment wywołuje setCount(count + 1), czyli setCount(1).
  5. React planuje ponowne renderowanie komponentu Counter.
  6. Podczas ponownego renderowania, useState(0) zwraca teraz [1, funkcja] (React pamięta aktualną wartość stanu). count ma wartość 1.
  7. Wyświetlamy nową wartość count (czyli 1).

Aktualizacje Stanu Mogą Być Asynchroniczne

React może grupować wiele wywołań setStateFunction w jedną aktualizację dla poprawy wydajności. Oznacza to, że nie możesz polegać na wartości stanu bezpośrednio po wywołaniu funkcji ustawiającej stan.

const incrementTwice = () => {
  setCount(count + 1); // count tutaj wciąż może być starą wartością
  setCount(count + 1); // obie aktualizacje mogą użyć tej samej starej wartości \'count\'
  // Efekt: licznik zwiększy się tylko o 1!
};

Aby poprawnie aktualizować stan w oparciu o jego poprzednią wartość, należy przekazać funkcję do setStateFunction. Ta funkcja otrzyma poprzednią wartość stanu jako argument.

const increment = () => {
  setCount(prevCount => prevCount + 1);
};

const incrementTwiceSafely = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
  // Efekt: licznik zwiększy się o 2
};

Zawsze używaj formy funkcyjnej, gdy nowa wartość stanu zależy od poprzedniej.

Wiele Zmiennych Stanowych

Możesz wywoływać useState wielokrotnie w jednym komponencie, aby zarządzać różnymi, niezależnymi częściami stanu.

function UserForm() {
  const [name, setName] = useState(\'\');
  const [email, setEmail] = useState(\'\');
  const [isActive, setIsActive] = useState(false);

  // ... obsługa formularza

  return (
    <form>
      <input type="text" value={name} onChange={e => setName(e.target.value)} placeholder="Imię" />
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />
      <label>
        Aktywny:
        <input type="checkbox" checked={isActive} onChange={e => setIsActive(e.target.checked)} />
      </label>
    </form>
  );
}

Stan z Obiektami i Tablicami

Możesz przechowywać obiekty i tablice w stanie. Pamiętaj jednak o zasadzie niemutowalności: nigdy nie modyfikuj bezpośrednio obiektów ani tablic w stanie. Zamiast tego, twórz nowe obiekty lub tablice z wprowadzonymi zmianami.

function ProfileEditor() {
  const [profile, setProfile] = useState({ name: \'Jan\", city: \'Warszawa\' });

  const handleNameChange = (event) => {
    const newName = event.target.value;
    // Tworzymy NOWY obiekt, kopiując stary i zmieniając \'name\'
    setProfile(prevProfile => ({
      ...prevProfile, // skopiuj wszystkie właściwości z prevProfile
      name: newName   // nadpisz właściwość \'name\'
    }));
  };

  return (
    <div>
      <p>Imię: {profile.name}, Miasto: {profile.city}</p>
      <input type="text" value={profile.name} onChange={handleNameChange} />
      {/* Podobnie dla \'city\' */}
    </div>
  );
}

Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/TextInputDisplay.jsx
import React, { useState } from \'react\';

function TextInputDisplay() {
  const [inputValue, setInputValue] = useState(\'\');

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <div>
      <label htmlFor="textInput">Wpisz tekst: </label>
      <input 
        type="text" 
        id="textInput"
        value={inputValue} 
        onChange={handleChange} 
      />
      <p>Wpisany tekst: {inputValue}</p>
    </div>
  );
}

export default TextInputDisplay;

// W App.jsx
import React from \'react\';
import TextInputDisplay from \'./TextInputDisplay\";

function App() {
  return (
    <div>
      <h1>Podgląd Wpisywanego Tekstu</h1>
      <TextInputDisplay />
    </div>
  );
}

export default App;

Cel: Stworzyć komponent, który pozwala użytkownikowi wpisać tekst w pole input, a wpisany tekst jest na bieżąco wyświetlany poniżej.

Kroki:

  1. Stwórz komponent funkcyjny, np. TextInputDisplay.
  2. Użyj useState, aby zainicjalizować stan dla wartości pola tekstowego (np. inputValue) pustym stringiem.
  3. Wyrenderuj element <input type="text" />.
  4. Powiąż wartość pola input ze stanem inputValue za pomocą atrybutu value.
  5. Dodaj obsługę zdarzenia onChange do pola input. W funkcji obsługi zdarzenia aktualizuj stan inputValue wartością z event.target.value.
  6. Poniżej pola input wyrenderuj paragraf <p>, który wyświetla aktualną wartość stanu inputValue.
  7. Użyj komponentu TextInputDisplay w App.jsx.

Zadanie do samodzielnego wykonania

Stwórz komponent "Przełącznik Koloru Tła". Komponent powinien:

  1. Mieć stan (np. isLight) przechowujący informację, czy tło jest jasne (true) czy ciemne (false), zainicjalizowany na true.
  2. Wyświetlać div, którego kolor tła zależy od stanu isLight (np. biały dla true, czarny dla false). Kolor tekstu wewnątrz diva również powinien się zmieniać dla kontrastu (np. czarny na białym, biały na czarnym).
  3. Wyświetlać przycisk "Przełącz tło".
  4. Po kliknięciu przycisku, stan isLight powinien zostać przełączony na przeciwną wartość (true -> false, false -> true), co spowoduje zmianę kolorów diva. Użyj formy funkcyjnej do aktualizacji stanu.

FAQ - Stan (State) - Hook useState

Gdzie powinienem deklarować `useState`?

Hook `useState` (i inne Hooki) należy wywoływać tylko na najwyższym poziomie komponentu funkcyjnego lub innego Hooka. Nie wolno ich wywoływać wewnątrz pętli, warunków (if) ani zagnieżdżonych funkcji. React polega na stałej kolejności wywołań Hooków przy każdym renderowaniu.

Czy mogę używać `useState` poza komponentem funkcyjnym?

Nie, Hooki, w tym `useState`, mogą być używane tylko wewnątrz komponentów funkcyjnych React lub wewnątrz niestandardowych Hooków (custom hooks). Nie można ich używać w zwykłych funkcjach JavaScript ani w komponentach klasowych.

Jaka jest różnica między `useState(\'\')` a `useState()`?

`useState(\'\')` inicjalizuje stan pustym stringiem. `useState()` (bez argumentu) inicjalizuje stan wartością `undefined`. Wybór zależy od tego, jaka wartość początkowa jest logiczna dla danego stanu. Często lepiej jest podać jawną wartość początkową (np. `null`, `\'\'`, `0`, `[]`, `{}`).

Dlaczego aktualizacje stanu są asynchroniczne?

React grupuje aktualizacje stanu, aby zoptymalizować wydajność. Zamiast renderować komponent na nowo po każdym pojedynczym wywołaniu `setState`, React może poczekać i wykonać wiele aktualizacji naraz w jednym cyklu renderowania. Poprawia to płynność działania aplikacji.

Czy nazwy `stateVariable` i `setStateFunction` są obowiązkowe?

Nie, możesz nazwać zmienną stanu i funkcję aktualizującą dowolnie, np. `const [user, setUser] = useState(null);`. Konwencją jest jednak używanie nazwy zmiennej (np. `count`) i funkcji z prefiksem "set" (np. `setCount`).

Co się stanie, jeśli jako wartość początkową `useState` podam wynik funkcji?

Jeśli podasz `useState(mojaFunkcja())`, funkcja `mojaFunkcja` zostanie wykonana przy każdym renderowaniu komponentu. Jeśli obliczenie wartości początkowej jest kosztowne, lepiej użyć "lazy initial state", przekazując funkcję do `useState`: `useState(() => mojaFunkcja())`. Wtedy funkcja zostanie wykonana tylko raz, podczas pierwszego renderowania.

Jak zarządzać bardziej złożonym stanem?

Dla prostych wartości `useState` jest idealny. Jeśli masz wiele powiązanych ze sobą pól stanu lub złożoną logikę aktualizacji, możesz rozważyć użycie Hooka `useReducer` (omówionego później) lub bibliotek do zarządzania stanem globalnym (jak Redux, Zustand, Context API).