Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz
2026-02-03
Wprowadzenie
W poprzednich wpisach stworzyliśmy solidny fundament: poznaliśmy TSX, nauczyliśmy się tworzyć komponenty funkcyjne i przekazywać dane przez props. Jednak wszystko, co do tej pory zbudowaliśmy, było statyczne. Komponenty wyświetlały dane, które otrzymały, ale nie reagowały na działania użytkownika.
Nadszedł czas, aby to zmienić. W tym wpisie poznamy stan komponentów (state) – mechanizm, który pozwala komponentom "zapamiętywać" dane i reagować na zmiany. Nauczymy się obsługiwać zdarzenia użytkownika (kliknięcia, wpisywanie tekstu), tworzyć formularze i renderować komponenty warunkowo w zależności od stanu.
To będzie przełomowy wpis – po nim Twoje komponenty ożyją i staną się prawdziwie interaktywne. Przygotuj się na dużo kodu i praktycznych przykładów!
Stan komponentu – State
W React dane dzielimy na dwa rodzaje:
- Props – dane otrzymane od rodzica (immutable, nie możemy ich zmieniać)
- State – dane własne komponentu (mutable, możemy je zmieniać)
Props poznaliśmy w poprzednim wpisie. Teraz czas na state.
State to prywatne dane komponentu, które mogą się zmieniać w czasie. Gdy state się zmienia, React automatycznie re-renderuje komponent z nowymi danymi.
Przykład ze świata rzeczywistego: licznik.
// ❌ To NIE zadziała poprawnie
function Counter() {
let count = 0; // Zwykła zmienna
function increment() {
count = count + 1;
console.log(count); // Wyświetli się w konsoli, ale UI się nie zaktualizuje!
}
return (
<div>
<p>Licznik: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
Dlaczego to nie działa? Bo zmiana zwykłej zmiennej nie powoduje re-renderowania komponentu. React nie wie, że coś się zmieniło.
useState Hook
Rozwiązaniem jest useState – Hook, który daje nam state w komponencie funkcyjnym.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
return (
<div>
<p>Licznik: {count}</p>
<button onClick={increment}>+1</button>
</div>
);
}
export default Counter;
To zadziała! Kliknięcie w przycisk zwiększy licznik i UI się zaktualizuje.
const [count, setCount] = useState(0);
Rozbijmy to na części:
- useState(0) – wywołanie Hooka z wartością początkową (initial state) równą 0
-
[count, setCount] – destrukturyzacja tablicy zwróconej przez useState:
- count – aktualna wartość state
- setCount – funkcja do zmiany state
- const – używamy const, bo nie zmieniamy bezpośrednio count, tylko przez setCount
TypeScript automatycznie wywnioskuje typ z wartości początkowej:
const [count, setCount] = useState(0); // TypeScript wie, że count: number
const [name, setName] = useState(''); // TypeScript wie, że name: string
const [isActive, setIsActive] = useState(false); // TypeScript wie, że isActive: boolean
Ale możemy też być explicit:
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
Generics przydają się szczególnie gdy:
- Wartość początkowa to null lub undefined
- Używamy złożonych typów
Możesz mieć wiele useState w jednym komponencie:
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
return (
// ...
);
}
Każdy useState jest niezależny. Zmiana jednego nie wpływa na pozostałe.
Czasem wygodniej jest trzymać związane dane w jednym obiekcie:
interface FormData {
firstName: string;
lastName: string;
age: number;
email: string;
}
function UserForm() {
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
age: 0,
email: ''
});
// Aktualizacja konkretnego pola
function updateFirstName(value: string) {
setFormData({
...formData,
firstName: value
});
}
return (
// ...
);
}
Ważne: Przy aktualizacji obiektu zawsze używaj spread operator (...) żeby skopiować pozostałe pola! State w React powinien być immutable.
// ❌ NIE RÓB TAK
formData.firstName = 'Paweł';
setFormData(formData);
// ✅ RÓB TAK
setFormData({
...formData,
firstName: 'Paweł'
});
Zdarzenia (Events) w React
Czas nauczyć się reagować na działania użytkownika: kliknięcia, wpisywanie, submit formularza, najechanie myszką, itp.
function Button() {
function handleClick() {
alert('Przycisk kliknięty!');
}
return <button onClick={handleClick}>Kliknij mnie</button>;
}
Ważne różnice od HTML:
- onClick (camelCase), nie onclick
- Przekazujemy referencję do funkcji, nie string: onClick={handleClick}, nie onclick="handleClick()"
Możesz też używać arrow functions bezpośrednio:
<button onClick={() => alert('Kliknięty!')}>Kliknij</button>
Ale uwaga – tworzy to nową funkcję przy każdym renderze. Dla prostych przypadków OK, dla złożonych lepiej zdefiniować funkcję osobno.
Funkcje obsługujące eventy otrzymują obiekt event:
function Button() {
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log('Kliknięto w:', event.currentTarget);
console.log('Pozycja X:', event.clientX);
console.log('Pozycja Y:', event.clientY);
}
return <button onClick={handleClick}>Kliknij mnie</button>;
}
TypeScript wymaga typowania event'a. Najczęstsze typy:
- React.MouseEvent<HTMLButtonElement> – kliknięcie w button
- React.ChangeEvent<HTMLInputElement> – zmiana w input
- React.FormEvent<HTMLFormElement> – submit formularza
- React.KeyboardEvent<HTMLInputElement> – zdarzenia klawiatury
Czasem potrzebujemy przekazać dodatkowe dane do handlera:
function ProductList() {
function handleDelete(productId: number) {
console.log('Usuwam produkt:', productId);
}
return (
<div>
<button onClick={() => handleDelete(1)}>Usuń produkt 1</button>
<button onClick={() => handleDelete(2)}>Usuń produkt 2</button>
</div>
);
}
Używamy arrow function, która wywołuje naszą funkcję z parametrem.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1);
}
function decrement() {
setCount(count - 1);
}
function reset() {
setCount(0);
}
return (
<div className="counter">
<h2>Licznik: {count}</h2>
<div className="buttons">
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
<button onClick={increment}>+1</button>
</div>
</div>
);
}
export default Counter;
Formularze w React
Formularze to serce większości aplikacji webowych. W React mamy dwa podejścia: controlled i uncontrolled components. Skupimy się na controlled – to standard w React.
W controlled component wartość pola formularza jest kontrolowana przez state:
import { useState } from 'react';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
function handleEmailChange(event: React.ChangeEvent<HTMLInputElement>) {
setEmail(event.target.value);
}
function handlePasswordChange(event: React.ChangeEvent<HTMLInputElement>) {
setPassword(event.target.value);
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); // Zapobiega przeładowaniu strony
console.log('Email:', email);
console.log('Password:', password);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
value={email}
onChange={handleEmailChange}
/>
</div>
<div>
<label>Hasło:</label>
<input
type="password"
value={password}
onChange={handlePasswordChange}
/>
</div>
<button type="submit">Zaloguj</button>
</form>
);
}
export default LoginForm;
Co tu się dzieje?
- State (email, password) przechowuje wartości pól
- value={email} – React kontroluje wartość inputa
- onChange – przy każdej zmianie aktualizujemy state
- onSubmit – obsługujemy wysłanie formularza
- event.preventDefault() – zapobiega domyślnej akcji (przeładowanie strony)
To jest single source of truth – state jest jedynym źródłem prawdy o wartości pola.
Zamiast wielu useState, użyjmy jednego z obiektem:
import { useState } from 'react';
interface LoginFormData {
email: string;
password: string;
}
function LoginForm() {
const [formData, setFormData] = useState<LoginFormData>({
email: '',
password: ''
});
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value // Computed property name
});
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log('Form data:', formData);
}
return (
<form onSubmit={handleSubmit}>
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<label>Hasło:</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</div>
<button type="submit">Zaloguj</button>
</form>
);
}
export default LoginForm;
Klucz: używamy name attribute w input i [name]: value (computed property) do aktualizacji odpowiedniego pola. Jedna funkcja handleChange obsługuje wszystkie pola!
React obsługuje wszystkie standardowe inputy:
import { useState } from 'react';
interface FormData {
username: string;
email: string;
age: number;
bio: string;
country: string;
newsletter: boolean;
gender: string;
}
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
age: 18,
bio: '',
country: 'PL',
newsletter: false,
gender: 'male'
});
function handleChange(
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) {
const { name, value, type } = event.target;
// Checkbox ma specjalną obsługę
const newValue = type === 'checkbox'
? (event.target as HTMLInputElement).checked
: value;
setFormData({
...formData,
[name]: newValue
});
}
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log('Registration data:', formData);
}
return (
<form onSubmit={handleSubmit}>
{/* Text input */}
<div>
<label>Nazwa użytkownika:</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</div>
{/* Email input */}
<div>
<label>Email:</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</div>
{/* Number input */}
<div>
<label>Wiek:</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
/>
</div>
{/* Textarea */}
<div>
<label>Bio:</label>
<textarea
name="bio"
value={formData.bio}
onChange={handleChange}
rows={4}
/>
</div>
{/* Select */}
<div>
<label>Kraj:</label>
<select name="country" value={formData.country} onChange={handleChange}>
<option value="PL">Polska</option>
<option value="US">USA</option>
<option value="UK">Wielka Brytania</option>
<option value="DE">Niemcy</option>
</select>
</div>
{/* Checkbox */}
<div>
<label>
<input
type="checkbox"
name="newsletter"
checked={formData.newsletter}
onChange={handleChange}
/>
Zapisz się do newslettera
</label>
</div>
{/* Radio buttons */}
<div>
<label>Płeć:</label>
<label>
<input
type="radio"
name="gender"
value="male"
checked={formData.gender === 'male'}
onChange={handleChange}
/>
Mężczyzna
</label>
<label>
<input
type="radio"
name="gender"
value="female"
checked={formData.gender === 'female'}
onChange={handleChange}
/>
Kobieta
</label>
</div>
<button type="submit">Zarejestruj się</button>
</form>
);
}
export default RegistrationForm;
Ważne różnice:
- Checkbox: używamy checked zamiast value, pobieramy event.target.checked
- Radio: używamy checked={formData.gender === 'male'} do określenia który jest wybrany
- Textarea: w React używa value (w HTML treść jest między tagami)
- Select: używa value na <select>, nie selected na <option>
Renderowanie warunkowe
Często chcemy pokazać/ukryć elementy w zależności od stanu lub props. W React mamy kilka sposobów na to.
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
if (isLoggedIn) {
return <h1>Witaj z powrotem!</h1>;
} else {
return <h1>Proszę się zalogować</h1>;
}
}
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<h1>
{isLoggedIn ? 'Witaj z powrotem!' : 'Proszę się zalogować'}
</h1>
);
}
function Mailbox({ unreadMessages }: { unreadMessages: number }) {
return (
<div>
<h1>Twoja skrzynka</h1>
{unreadMessages > 0 && (
<p>Masz {unreadMessages} nieprzeczytanych wiadomości</p>
)}
</div>
);
}
&& działa tak: jeśli lewa strona jest true, renderuj prawą stronę. Jeśli false, nie renderuj nic.
Uwaga: Jeśli lewa strona to 0, zostanie wyrenderowane 0 (bo 0 jest falsy ale też wartością)!
// ❌ Problem
{count && <p>Count: {count}</p>} // Gdy count=0, wyświetli się "0"
// ✅ Rozwiązanie
{count > 0 && <p>Count: {count}</p>}
Dla złożonej logiki:
function UserStatus({ user }: { user: User | null }) {
let content;
if (!user) {
content = <p>Ładowanie...</p>;
} else if (user.isPremium) {
content = <p>Witaj użytkowniku Premium, {user.name}!</p>;
} else if (user.isActive) {
content = <p>Witaj {user.name}</p>;
} else {
content = <p>Twoje konto jest nieaktywne</p>;
}
return <div>{content}</div>;
}
import { useState } from 'react';
function ToggleContent() {
const [isVisible, setIsVisible] = useState(false);
function toggle() {
setIsVisible(!isVisible);
}
return (
<div>
<button onClick={toggle}>
{isVisible ? 'Ukryj' : 'Pokaż'} treść
</button>
{isVisible && (
<div className="content">
<p>To jest ukryta treść!</p>
<p>Możesz ją pokazać lub ukryć klikając przycisk.</p>
</div>
)}
</div>
);
}
export default ToggleContent;
Listy i key
Często musimy renderować tablicę danych jako listę komponentów.
function TodoList() {
const todos = ['Kupić mleko', 'Napisać kod', 'Pójść na siłownię'];
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
);
}
map() iteruje po tablicy i zwraca nową tablicę elementów JSX.
Każdy element listy MUSI mieć unikalny key. React używa key do identyfikacji, które elementy się zmieniły, zostały dodane lub usunięte.
// ❌ Źle - brak key
{todos.map(todo => <li>{todo}</li>)}
// ⚠️ Niezalecane - index jako key
{todos.map((todo, index) => <li key={index}>{todo}</li>)}
// ✅ Dobrze - unikalny identyfikator
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
Dlaczego index jest zły? Jeśli zmienisz kolejność elementów (sortowanie, dodawanie na początku), index się zmienia i React może błędnie zidentyfikować elementy.
Połączmy state, zdarzenia i listy w kompletną aplikację Todo:
import { useState } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Nauczyć się React', completed: false },
{ id: 2, text: 'Zbudować projekt', completed: false }
]);
const [inputValue, setInputValue] = useState('');
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
setInputValue(event.target.value);
}
function addTodo() {
if (inputValue.trim() === '') return;
const newTodo: Todo = {
id: Date.now(), // Prosty sposób na unikalne ID
text: inputValue,
completed: false
};
setTodos([...todos, newTodo]);
setInputValue(''); // Wyczyść input
}
function toggleTodo(id: number) {
setTodos(
todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
}
function deleteTodo(id: number) {
setTodos(todos.filter(todo => todo.id !== id));
}
return (
<div className="todo-app">
<h1>Todo List</h1>
<div className="add-todo">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder="Dodaj nowe zadanie..."
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Dodaj</button>
</div>
<ul className="todo-list">
{todos.map(todo => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Usuń</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p className="empty-state">Brak zadań. Dodaj pierwsze!</p>
)}
</div>
);
}
export default TodoApp;
Dodajmy style (App.css):
.todo-app {
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.add-todo {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-todo input {
flex: 1;
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.add-todo button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.add-todo button:hover {
background-color: #45a049;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #888;
}
.todo-list span {
flex: 1;
}
.todo-list button {
padding: 5px 10px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-list button:hover {
background-color: #da190b;
}
.empty-state {
text-align: center;
color: #888;
font-style: italic;
margin-top: 20px;
}
Co tu się dzieje?
- State: todos (tablica zadań) i inputValue (wartość inputa)
- addTodo: tworzy nowe todo, dodaje do tablicy, czyści input
- toggleTodo: zmienia completed dla konkretnego todo (używamy map do immutable update)
- deleteTodo: usuwa todo (używamy filter)
- Key: każde todo ma unikalny id używany jako key
- Renderowanie warunkowe: pokazujemy komunikat gdy lista jest pusta
To kompletna, działająca aplikacja z pełnym TypeScript support! 🎉
Immutability – dlaczego to ważne?
Zauważyłeś, że ciągle używamy spread operator (...) i metod jak map, filter zamiast mutować state bezpośrednio?
// ❌ NIE RÓB TAK - mutacja
todos.push(newTodo);
setTodos(todos);
// ✅ RÓB TAK - immutability
setTodos([...todos, newTodo]);
Dlaczego?
- React porównuje referencje – jeśli zmienisz obiekt in-place, referencja pozostaje ta sama i React może nie wykryć zmiany
- Przewidywalność – immutable updates są bezpieczniejsze i łatwiejsze do debugowania
- Time-travel debugging – narzędzia jak Redux DevTools polegają na immutability
- Optymalizacja – React.memo i inne optymalizacje działają lepiej z immutable data
Metody immutable:
- Dodaj do tablicy: [...array, newItem]
- Usuń z tablicy: array.filter(item => item.id !== id)
- Zmień element tablicy: array.map(item => item.id === id ? {...item, changed: true} : item)
- Zmień właściwość obiektu: {...object, property: newValue}
Best Practices dla state i eventów
// ❌ Źle
function handle() { }
function click() { }
// ✅ Dobrze
function handleSubmit() { }
function handleUserDelete() { }
function handleEmailChange() { }
Konwencja: handle + Co + JakieZdarzenie
// ❌ Źle - state w App, używany tylko w TodoList
function App() {
const [todos, setTodos] = useState([]);
return <TodoList todos={todos} setTodos={setTodos} />;
}
// ✅ Dobrze - state bezpośrednio w TodoList
function TodoList() {
const [todos, setTodos] = useState([]);
// ...
}
Wyjątek: Jeśli state jest używany przez wiele komponentów, przenieś go wyżej (lifting state up – o tym w kolejnych wpisach).
// ❌ Złe - mieszamy niezależne dane
const [data, setData] = useState({
user: null,
posts: [],
theme: 'dark',
sidebarOpen: false
});
// ✅ Lepiej - osobne state'y
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [theme, setTheme] = useState('dark');
const [sidebarOpen, setSidebarOpen] = useState(false);
// ⚠️ Może nie zadziałać poprawnie przy wielu szybkich aktualizacjach
setCount(count + 1);
// ✅ Bezpieczniejsze
setCount(prevCount => prevCount + 1);
React może batchować (grupować) aktualizacje, więc count może być przestarzały. Functional update zawsze operuje na aktualnej wartości.
Podsumowanie
W tym wpisie nauczyliśmy się kluczowych konceptów React:
- ✅ useState Hook – zarządzanie stanem komponentu
- ✅ Zdarzenia – obsługa interakcji użytkownika (click, change, submit)
- ✅ Formularze – controlled components, różne typy inputów
- ✅ Renderowanie warunkowe – pokazywanie/ukrywanie elementów na podstawie warunków
- ✅ Listy i key – renderowanie tablic danych
- ✅ Immutability – dlaczego nie mutujemy state bezpośrednio
- ✅ Praktyczny projekt – kompletna aplikacja Todo List
To był przełomowy wpis! Twoje komponenty już nie są statyczne – reagują na użytkownika, zapamiętują dane, aktualizują się dynamicznie. To właśnie ta interaktywność sprawia, że React jest tak potężny.
W kolejnym wpisie zagłębimy się w cykl życia komponentów i poznamy useEffect Hook – nauczymy się wykonywać efekty uboczne (side effects) jak pobieranie danych z API, subskrypcje, timery. Zrozumiemy też jak React renderuje komponenty i kiedy to robi.
Zanim przejdziesz do kolejnego wpisu, spróbuj rozszerzyć naszą aplikację Todo:
- Dodaj filtrowanie: "Wszystkie", "Aktywne", "Ukończone"
- Dodaj licznik aktywnych zadań
- Dodaj przycisk "Usuń wszystkie ukończone"
- Zapisuj todos w localStorage (to już lekka zapowiedź useEffect!)
Do zobaczenia w części czwartej! 🚀