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:
- Propsy są przekazywane do komponentu z zewnątrz (przez rodzica) i są niezmienne (read-only) wewnątrz komponentu.
- Stan jest zarządzany wewnątrz komponentu, może być inicjalizowany wartością początkową i może być modyfikowany przez sam komponent.
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
}
- Importujemy
useState
z biblioteki React. - Wywołujemy
useState(initialValue)
wewnątrz komponentu funkcyjnego. ArgumentinitialValue
to początkowa wartość stanu (używana tylko podczas pierwszego renderowania). useState
zwraca tablicę zawierającą dwie wartości, które zazwyczaj destrukturyzujemy:stateVariable
: Aktualna wartość stanu.setStateFunction
: Funkcja, której używamy do aktualizacji wartości stanu.
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?
- Podczas pierwszego renderowania,
useState(0)
zwraca[0, funkcja]
.count
ma wartość 0. - Wyświetlamy
count
(czyli 0) w paragrafie. - Gdy użytkownik klika przycisk, wywoływana jest funkcja
increment
. increment
wywołujesetCount(count + 1)
, czylisetCount(1)
.- React planuje ponowne renderowanie komponentu
Counter
. - Podczas ponownego renderowania,
useState(0)
zwraca teraz[1, funkcja]
(React pamięta aktualną wartość stanu).count
ma wartość 1. - 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:
- Stwórz komponent funkcyjny, np.
TextInputDisplay
. - Użyj
useState
, aby zainicjalizować stan dla wartości pola tekstowego (np.inputValue
) pustym stringiem. - Wyrenderuj element
<input type="text" />
. - Powiąż wartość pola input ze stanem
inputValue
za pomocą atrybutuvalue
. - Dodaj obsługę zdarzenia
onChange
do pola input. W funkcji obsługi zdarzenia aktualizuj staninputValue
wartością zevent.target.value
. - Poniżej pola input wyrenderuj paragraf
<p>
, który wyświetla aktualną wartość stanuinputValue
. - Użyj komponentu
TextInputDisplay
wApp.jsx
.
Zadanie do samodzielnego wykonania
Stwórz komponent "Przełącznik Koloru Tła". Komponent powinien:
- Mieć stan (np.
isLight
) przechowujący informację, czy tło jest jasne (true
) czy ciemne (false
), zainicjalizowany natrue
. - Wyświetlać div, którego kolor tła zależy od stanu
isLight
(np. biały dlatrue
, czarny dlafalse
). Kolor tekstu wewnątrz diva również powinien się zmieniać dla kontrastu (np. czarny na białym, biały na czarnym). - Wyświetlać przycisk "Przełącz tło".
- 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).