Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
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.

Czym jest 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.

Anatomia useState

const [count, setCount] = useState(0);

Rozbijmy to na części:

  1. useState(0) – wywołanie Hooka z wartością początkową (initial state) równą 0
  2. [count, setCount] – destrukturyzacja tablicy zwróconej przez useState:
    • count – aktualna wartość state
    • setCount – funkcja do zmiany state
  3. const – używamy const, bo nie zmieniamy bezpośrednio count, tylko przez setCount

TypeScript w useState

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

Wiele state'ów w komponencie

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.

State jako obiekt

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.

onClick – obsługa kliknięć

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()"

Inline handlers

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.

Event object i TypeScript

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

Przekazywanie parametrów do handlerów

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.

Praktyczny przykład: Licznik z trzema przyciskami

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.

Controlled Components

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?

  1. State (email, password) przechowuje wartości pól
  2. value={email} – React kontroluje wartość inputa
  3. onChange – przy każdej zmianie aktualizujemy state
  4. onSubmit – obsługujemy wysłanie formularza
  5. 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.

Refaktoryzacja z obiektem state

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!

Różne typy inputów

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.

1. If/else w funkcji

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
  if (isLoggedIn) {
    return <h1>Witaj z powrotem!</h1>;
  } else {
    return <h1>Proszę się zalogować</h1>;
  }
}

2. Ternary operator

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
  return (
    <h1>
      {isLoggedIn ? 'Witaj z powrotem!' : 'Proszę się zalogować'}
    </h1>
  );
}

3. Logical AND (&&)

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>}

4. Zmienna pomocnicza

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>;
}

Praktyczny przykład: Toggle content

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.

map() do renderowania list

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.

Atrybut key

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.

Praktyczny przykład: Todo List

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?

  1. State: todos (tablica zadań) i inputValue (wartość inputa)
  2. addTodo: tworzy nowe todo, dodaje do tablicy, czyści input
  3. toggleTodo: zmienia completed dla konkretnego todo (używamy map do immutable update)
  4. deleteTodo: usuwa todo (używamy filter)
  5. Key: każde todo ma unikalny id używany jako key
  6. 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?

  1. React porównuje referencje – jeśli zmienisz obiekt in-place, referencja pozostaje ta sama i React może nie wykryć zmiany
  2. Przewidywalność – immutable updates są bezpieczniejsze i łatwiejsze do debugowania
  3. Time-travel debugging – narzędzia jak Redux DevTools polegają na immutability
  4. 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

1. Nazywaj handlery opisowo

// ❌ Źle
function handle() { }
function click() { }

// ✅ Dobrze
function handleSubmit() { }
function handleUserDelete() { }
function handleEmailChange() { }

Konwencja: handle + Co + JakieZdarzenie

2. Trzymaj state jak najbliżej miejsca użycia

// ❌ Ź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).

3. Jeden state = jedna odpowiedzialność

// ❌ 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);

4. Używaj functional updates gdy nowy state zależy od starego

// ⚠️ 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.

Zadanie dla Ciebie

Zanim przejdziesz do kolejnego wpisu, spróbuj rozszerzyć naszą aplikację Todo:

  1. Dodaj filtrowanie: "Wszystkie", "Aktywne", "Ukończone"
  2. Dodaj licznik aktywnych zadań
  3. Dodaj przycisk "Usuń wszystkie ukończone"
  4. Zapisuj todos w localStorage (to już lekka zapowiedź useEffect!)

Do zobaczenia w części czwartej! 🚀

Czwarta część artykułu: React - wprowadzenie: część IV (useEffect Hook, cykl życia komponentów, efekty uboczne, dependency array)