Realizacja Aplikacji Internetowych

Rok akademicki: 2026/27

Instrukcja laboratoryjna: Architektura Biblioteki React 19, Składnia JSX/TSX, Mechanizm Fiber, Hooki, TanStack Router & Query, Zustand oraz Testowanie (Vitest i Playwright)

Prowadzący: Tomasz Goluch
Wersja: 1.0


1. Wprowadzenie: Filozofia React, Virtual DOM, Architektura Fiber i React Compiler

Cel: Zrozumienie deklaratywnego paradygmatu budowy interfejsów użytkownika (UI = f(state)), jednokierunkowego przepływu danych, silnika uzgadniania React Fiber oraz automatycznej optymalizacji przez React Compiler.

React (rozwijany przez firmę Meta oraz społeczność open-source) to deklaratywna, oparta na komponentach biblioteka JavaScript/TypeScript służąca do budowania interfejsów użytkownika. W odróżnieniu od monolitycznych frameworków (takich jak Angular), React koncentruje się wyłącznie na warstwie widoku, pozostawiając dobór narzędzi do routingu, zarządzania stanem czy komunikacji sieciowej otwartemu ekosystemowi.

1. Paradygmat deklaratywny: UI = f(state)

W podejściu imperatywnym (czysty JavaScript / jQuery) programista musi ręcznie instruować przeglądarkę, jak krok po kroku zmodyfikować drzewo DOM po każdej akcji użytkownika (document.createElement, element.classList.add). W React interfejs użytkownika jest czystą funkcją stanu:

2. Architektura React Fiber i Proces Uzgadniania (Reconciliation)

Bezpośrednie manipulacje na drzewie DOM przeglądarki są kosztowne, ponieważ wymuszają przeliczanie układu geometrycznego (Reflow / Layout) oraz ponowne odrysowanie pikseli (Repaint). React rozwiązuje ten problem za pomocą wirtualnej reprezentacji drzewa oraz silnika React Fiber (wprowadzonego w React 16 i rozwiniętego o Concurrent Rendering w React 18/19).

Proces aktualizacji interfejsu dzieli się na dwie ściśle odseparowane fazy:

Zmiana Stanu (setState / Akcja w Store / Nowe Props)
                        │
                        ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. FAZA RENDEROWANIA (Render Phase) — Asynchroniczna i Przerywalna      │
│  • Wywołanie funkcji komponentów w pamięci RAM (czysta kalkulacja).     │
│  • Budowa nowego drzewa roboczego "workInProgress Fiber".               │
│  • Algorytm Diffingu (Reconciliation): porównanie z drzewem "current"   │
│    i oznaczenie węzłów wymagających zmian (flagi Placement/Update/Del). │
└───────────────────────┬─────────────────────────────────────────────────┘
                        │ Lista efektów (Effect List)
                        ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. FAZA ZATWIERDZANIA (Commit Phase) — Synchroniczna i Nieprzerywalna   │
│  • Zaaplikowanie wyliczonych różnic do fizycznego drzewa DOM.           │
│  • Synchroniczne wykonanie hooków useLayoutEffect (przed odrysowaniem). │
│  • Odrysowanie ekranu przez przeglądarkę (Browser Paint).               │
│  • Asynchroniczne wykonanie hooków useEffect (oraz funkcji cleanup).    │
└─────────────────────────────────────────────────────────────────────────┘

3. Porównanie architektoniczne: React vs Vue vs Angular

Kryterium React 19 Vue 3.5+ Angular (18/19+)
Typ rozwiązania Biblioteka warstwy UI + otwarty ekosystem. Progresywny framework (oficjalny Router i Pinia). Kompletny, opiniotwórczy framework korporacyjny.
Mechanizm reaktywności Jawna niemodyfikowalność (useState / nowe referencje) + React Compiler. Drobnoziarnista reaktywność oparta na obiektach ES6 Proxy (ref, reactive). Signals (Sygnały) oraz detekcja zmian (Zone.js / Zoneless).
Warstwa szablonów JSX / TSX (pełna moc języka TypeScript/JS wewnątrz znaczników). Jednoplikowe komponenty SFC (.vue) z sekcją <template> (lub opcjonalnie JSX). Szablony HTML rozszerzone o dyrektywy strukturalne (@if, @for).
Przepływ danych Ściśle jednokierunkowy (One-Way Data Binding: Props Down, Callbacks Up). Jednokierunkowy dla propsów + lukier składniowy dla wiązania dwukierunkowego (v-model). Jednokierunkowy ([prop], (event)) oraz dwukierunkowy ([(ngModel)]).

🧠 Nowość w React 19: React Compiler (Automatyczna Memoizacja)
W klasycznym React (wersje 16–18) zmiana stanu w komponencie nadrzędnym powodowała domyślnie ponowne wywołanie funkcji wszystkich komponentów potomnych, a programista musiał ręcznie optymalizować kod za pomocą hooków useMemo, useCallback oraz React.memo.
W React 19 wprowadzono React Compiler (babel-plugin-react-compiler), który na etapie budowania aplikacji analizuje przepływ danych w kodzie TSX i automatycznie memoizuje wartości pośrednie oraz poddrzewa JSX. Dzięki temu komponenty odświeżają tylko te fragmenty UI, których zależności faktycznie uległy zmianie, zbliżając wygodę pracy w React do drobnoziarnistej reaktywności znanej z Vue czy SolidJS!


2. Środowisko Uruchomieniowe, Instalacja (NVM, Vite, SWC) i Konteneryzacja

Cel: Konfiguracja środowiska Node.js, menedżerów pakietów, inicjalizacja nowoczesnego projektu React + TypeScript w oparciu o Vite oraz konfiguracja kontenerów Docker i edytorów IDE.

1. Zarządzanie wersjami Node.js (NVM) i menedżery pakietów

Do pracy z narzędziami budowania (Vite) oraz menedżerami pakietów (npm, pnpm, yarn) wymagane jest środowisko wykonawcze Node.js (zalecana aktualna wersja LTS).

Zamiast instalować pojedynczą, sztywną wersję Node.js, zaleca się użycie menedżera wersji NVM (Node Version Manager) – dla systemu Windows dostępnego pod adresem: [https://github.com/coreybutler/nvm-windows/releases/](https://github.com/coreybutler/nvm-windows/releases/).

Podstawowe komendy NVM (w terminalu z uprawnieniami administratora):

Alternatywne menedżery pakietów oraz narzędzia graficzne:

# Instalacja szybkiego menedżera pnpm (oszczędzającego miejsce na dysku dzięki twardym dowiązaniom) lub yarn:
npm install -g pnpm
npm install -g yarn

# Opcjonalny graficzny interfejs zarządzania zależnościami z package.json:
npm install -g npm-gui
npm-gui

2. Szybkie prototypowanie z CDN (ESM) vs Środowisko Produkcyjne (Vite)

W celach czysto demonstracyjnych (bez instalacji Node.js i kompilacji JSX) React 19 można załadować bezpośrednio w pliku HTML jako natywny moduł ECMAScript (type="module") za pośrednictwem serwera CDN esm.sh:

<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="UTF-8">
  <title>React 19 ESM bez bundlera</title>
  <script type="importmap">
    {
      "imports": {
        "react": "https://esm.sh/react@19",
        "react-dom/client": "https://esm.sh/react-dom@19/client"
      }
    }
  </script>
</head>
<body>
  <div id="root"></div>
  <script type="module">
    import React, { useState } from 'react';
    import { createRoot } from 'react-dom/client';

    function App() {
      const [count, setCount] = useState(0);
      return React.createElement(
        'button',
        { onClick: () => setCount(c => c + 1) },
        `Kliknięto: ${count} razy`
      );
    }

    createRoot(document.getElementById('root')).render(React.createElement(App));
  </script>
</body>
</html>

W rzeczywistych projektach (korzystających ze składni JSX/TSX, TypeScriptu, optymalizacji zasobów i Hot Module Replacement) zawsze korzystamy z narzędzia Vite.

⚠️ Wycofanie narzędzia Create React App (create-react-app):
Dawniej popularna komenda npx create-react-app (oparta na Webpacku) została oficjalnie wycofana (deprecated) przez zespół Reacta ze względu na powolny czas startu i przestarzałą architekturę. Obecnie standardem dla aplikacji SPA w React jest Vite (wykorzystujący natywne moduły ESM w przeglądarce oraz kompilator SWC napisany w języku Rust lub Babel).

3. Inicjalizacja nowego projektu React + TypeScript za pomocą Vite

Tworzymy nowy projekt za pomocą kreatora create-vite:

npm create vite@latest rai-react-lab -- --template react-swc-ts

(Można również wywołać interaktywne npm create vite@latest, wpisać nazwę projektu, wybrać framework React, a następnie wariant TypeScript + SWC lub TypeScript).

Instalacja zależności i uruchomienie serwera deweloperskiego:

cd rai-react-lab
npm install
npm run dev

Po ułamku sekundy serwer deweloperski Vite wystartuje pod domyślnym adresem http://localhost:5173/. Aby zatrzymać serwer w terminalu, naciśnij q + Enter (lub Ctrl + C).

4. Struktura katalogów projektu React (Vite + TypeScript)

rai-react-lab/
├── public/                 # Zasoby statyczne kopiowane 1:1 (np. favicon, robots.txt)
├── src/
│   ├── assets/             # Obrazy, czcionki i ikony przetwarzane przez bundler Vite
│   ├── components/         # Reużywalne komponenty UI (np. Button.tsx, Modal.tsx)
│   ├── hooks/              # Własne Hooki (Custom Hooks, np. useFetch.ts, useMousePosition.ts)
│   ├── pages/ (lub routes/)# Widoki podstron / trasy dla routera
│   ├── stores/             # Globalne magazyny stanu (np. Zustand: useUserStore.ts)
│   ├── App.tsx             # Główny komponent korzenia aplikacji
│   ├── main.tsx            # Punkt wejścia aplikacji (montowanie React w #root)
│   └── index.css           # Globalne arkusze stylów (np. import Tailwind CSS)
├── index.html              # Główny szablon HTML z kontenerem <div id="root"></div>
├── package.json            # Skrypty uruchomieniowe oraz lista zależności npm
├── tsconfig.json           # Konfiguracja kompilatora TypeScript
└── vite.config.ts          # Konfiguracja bundlera Vite i wtyczek (np. @vitejs/plugin-react-swc)

5. Konfiguracja w Visual Studio (*.esproj) oraz konteneryzacja w Dockerze

Konfiguracja projektu React w środowisku Microsoft Visual Studio 2022 (*.esproj): Jeśli projekt React ma wchodzić w skład solucji .sln w pełnym środowisku Visual Studio (np. obok backendu ASP.NET Core Web API), tworzymy w katalogu projektu plik rai-react-lab.esproj:

<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.1184077">
  <PropertyGroup>
    <StartupCommand>npm run dev</StartupCommand>
    <JavaScriptTestRoot>src\</JavaScriptTestRoot>
    <JavaScriptTestFramework>Vitest</JavaScriptTestFramework>
    <ShouldRunBuildScript>false</ShouldRunBuildScript>
    <BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
  </PropertyGroup>
</Project>

Uruchomienie środowiska laboratoryjnego w kontenerze Docker (docker-compose.yml): Aby uruchomić serwer deweloperski Vite wewnątrz kontenera Docker z działającym przeładowywaniem na żywo (HMR), w package.json dodajemy flagę --host do skryptu dev ("dev": "vite --host"), a następnie tworzymy plik docker-compose.yml:

services:
  react-app:
    image: node:22-alpine
    working_dir: /app
    volumes:
      - .:/app
      - /app/node_modules
    ports:
      - "5173:5173"
    environment:
      - CHOKIDAR_USEPOLLING=true
    command: sh -c "npm install && npm run dev"

Uruchomienie: docker compose up.


3. Punkt Wejścia, Składnia JSX/TSX, Renderowanie Warunkowe, Listy i Zdarzenia

Cel: Poznanie procesu montowania aplikacji, reguł składni JSX/TSX, wiązania właściwości, renderowania warunkowego, pętli .map() oraz komponentów kontrolowanych.

1. Punkt wejścia aplikacji (index.html oraz src/main.tsx)

W pliku index.html znajduje się pusty kontener DOM <div id="root"></div>. W pliku src/main.tsx inicjalizujemy korzeń aplikacji za pomocą metody createRoot z pakietu react-dom/client:

// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';

const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Nie znaleziono elementu #root w drzewie DOM');

createRoot(rootElement).render(
  <StrictMode>
    <App />
  </StrictMode>
);

(Znacznik <StrictMode> to narzędzie diagnostyczne działające wyłącznie w trybie deweloperskim – celowo wykonuje podwójne renderowanie i podwójny cykl montażu efektów, aby wcześnie wykryć błędy związane z nieczystymi funkcjami lub brakiem zwalniania zasobów).

2. Czym jest składnia JSX / TSX?

JSX (JavaScript XML) oraz jej typowany odpowiednik TSX (TypeScript XML) to rozszerzenie składni pozwalające pisać znaczniki przypominające HTML bezpośrednio wewnątrz kodu JavaScript/TypeScript. Przeglądarki nie rozumieją składni JSX – kompilator (SWC / Babel) tłumaczy każdy znacznik na wywołanie funkcji _jsx() z modułu react/jsx-runtime:

// 1. Kod napisany przez programistę w pliku .tsx:
const element = <h2 className="naglowek">Sufit z 7,45 to: {Math.ceil(7.45)}</h2>;

// 2. Kod wynikowy JavaScript po transpilacji (React 17+ Automatic JSX Runtime):
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h2', {
  className: 'naglowek',
  children: ['Sufit z 7,45 to: ', Math.ceil(7.45)]
});

Kluczowe reguły składni JSX/TSX:

  1. Jeden element nadrzędny (lub Fragment <>...</>): Komponent musi zwracać pojedynczy węzeł korzenia. Aby uniknąć tworzenia zbędnych znaczników <div> w DOM, grupujemy elementy w tzw. Fragment (<React.Fragment> lub skrócone <> ... </>).
  2. Nazwy atrybutów w notacji camelCase: Ponieważ kod JSX kompiluje się do właściwości obiektów DOM w JavaScript, zamiast atrybutu HTML class używamy className, a zamiast for w etykietach <label> używamy htmlFor.
  3. Obowiązkowe zamykanie wszystkich znaczników: Nawet pojedyncze znaczniki HTML muszą być samozamykające, np. <input />, <img src="..." alt="..." />, <br />.
  4. Wyrażenia JavaScript w pojedynczych nawiasach klamrowych { }: Wewnątrz { } można umieścić dowolne wyrażenie zwracające wartość (zmienną, działanie matematyczne, wywołanie funkcji Math.ceil(7.45) czy operator trójargumentowy).

    🛡️ Ochrona przed atakami XSS: React domyślnie escapuje (koduje do bezpiecznych encji tekstowych) wszystkie wartości wstawiane wewnątrz { } przed wyrenderowaniem ich w DOM.


3. Od dyrektyw do czystego TypeScriptu: Odpowiedniki konstrukcji szablonowych w React

W przeciwieństwie do Vue (używającego specjalnych dyrektyw v-if, v-for, v-bind, v-on, v-model), React wykorzystuje natywne konstrukcje języka JavaScript/TypeScript:

Mechanizm w interfejsie Realizacja w Vue (<template>) Realizacja w React (JSX / TSX)
Interpolacja tekstu {{ title }} {title}
Wiązanie atrybutu :src="path + filename" src={path + filename}
Wiązanie klas CSS :class="{ active: isChecked }" className={isChecked ? 'checked_class' : 'unchecked_class'}
Renderowanie warunkowe v-if="isChecked" / v-else {isChecked ? <span>Tak</span> : <span>Nie</span>} lub {isChecked && <span>Tak</span>}
Ukrywanie przez CSS v-show="isChecked" <span style={{ display: isChecked ? 'inline' : 'none' }}> (lub klasa hidden)
Iteracja po tablicy v-for="pet in tab" :key="pet.id" {tab.map(pet => <li key={pet.id}>{pet.name}</li>)}
Obsługa zdarzeń @click="increment" onClick={increment}
Wiązanie dwukierunkowe v-model="title" Komponent kontrolowany: value={title} + onChange={e => setTitle(e.target.value)}

4. Kompletny przykład: Komponenty kontrolowane, listy z kluczem key, warunki i zdarzenia

// src/App.tsx
import { useState, type ChangeEvent } from 'react';

interface Pet {
  id: number;
  name: string;
  checked: boolean;
}

export default function App() {
  // 1. Stan komponentu (useState)
  const [title, setTitle] = useState<string>('Moja aplikacja React 19');
  const [counter, setCounter] = useState<number>(0);
  const [pets, setPets] = useState<Pet[]>([
    { id: 1, name: 'kot', checked: true },
    { id: 2, name: 'pies', checked: false },
    { id: 3, name: 'mysz', checked: false },
  ]);

  const logoPath = 'https://upload.wikimedia.org/wikipedia/commons/a/a7/React-icon.svg';

  // 2. Niemodyfikowalna aktualizacja elementu tablicy (Immutability!)
  const togglePet = (id: number) => {
    setPets(prevPets =>
      prevPets.map(pet =>
        pet.id === id ? { ...pet, checked: !pet.checked } : pet
      )
    );
  };

  // 3. Dodanie nowego elementu do tablicy (nowa referencja tablicy [...prev, newItem])
  const addFox = () => {
    setPets(prevPets => [
      ...prevPets,
      { id: Date.now(), name: 'lis', checked: false }
    ]);
  };

  return (
    <div className="container" style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      {/* Wiązanie dynamicznego atrybutu src */}
      <img src={logoPath} height={42} alt="Logo React" />

      {/* Interpolacja zmiennej oraz wyrażenia JS */}
      <h2>{title}</h2>
      <p>Sufit z 7,45 to: {Math.ceil(7.45)}</p>

      {/* Komponent kontrolowany (odpowiednik dwukierunkowego v-model) */}
      <input
        type="text"
        value={title}
        onChange={(e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)}
        placeholder="Zmień tytuł aplikacji"
      />

      <hr />

      {/* Renderowanie listy za pomocą .map() oraz warunkowe klasy i elementy */}
      <h3>Lista zwierząt:</h3>
      <ul>
        {pets.map((pet, index) => (
          <li
            key={pet.id}
            className={pet.checked ? 'checked_class' : 'unchecked_class'}
          >
            <label>
              <input
                type="checkbox"
                checked={pet.checked}
                onChange={() => togglePet(pet.id)}
              />
              <span> {index}. </span>
              {pet.checked ? (
                <strong>{pet.name}</strong>
              ) : (
                <em style={{ color: 'gray' }}>removed ({pet.name})</em>
              )}
            </label>
          </li>
        ))}
      </ul>

      <button onClick={addFox}>Dodaj lisa do tablicy</button>

      <hr />

      {/* Obsługa zdarzeń kliknięcia */}
      <button onClick={() => setCounter(prev => prev + 1)}>
        Dodaj 1
      </button>
      <p>Przycisk naciśnięto {counter} razy.</p>
    </div>
  );
}

⚠️ Dlaczego atrybut key w metodzie .map() jest obowiązkowy i dlaczego nie wolno używać w nim indeksu tablicy index przy dynamicznych listach?
Arybut key pozwala algorytmowi Diffingu (Reconciliation) jednoznacznie powiązać element z poprzedniego renderowania z elementem po zmianie tablicy. Jeśli jako key podasz bieżący indeks tablicy (key={index}), a następnie usuniesz pierwszy element listy lub zmienisz kolejność sortowania, indeksy wszystkich pozostałych elementów przesuną się o 1. React pomyśli, że pod kluczem 0 zmieniła się zawartość i może błędnie zachować stan wewnętrzny kontrolek (np. fokus lub tekst w <input>) przy niewłaściwym wierszu! Jako key zawsze przekazuj stabilny, unikalny identyfikator rekordu (key={pet.id}).


4. Architektura Komponentów: Props, Komunikacja Rodzic ⇄ Potomek oraz Projekcja Treści (children)

Cel: Tworzenie reużywalnych komponentów funkcyjnych, silne typowanie właściwości wejściowych (Props), przekazywanie danych w górę drzewa przez funkcje zwrotne (Callbacks) oraz wstrzykiwanie treści przez właściwość children.

1. Jednokierunkowy przepływ danych (One-Way Data Flow)

W React dane przepływają ściśle z góry na dół (od komponentu nadrzędnego do podrzędnego) za pośrednictwem obiektu props.

          ┌───────────────────────────────────────────────┐
          │         Komponent Nadrzędny (Parent)          │
          │         Stan: const [msg, setMsg] = ...       │
          └───────────┬───────────────────────▲───────────┘
                      │                       │
      1. Props w dół: │ value={msg}           │ 2. Callback w górę:
                      │                       │    onValueChange(newText)
                      ▼                       │
          ┌───────────────────────────────────┴───────────┐
          │         Komponent Potomny (CustomInput)       │
          │   <input value={value}                        │
          │          onChange={e => onValueChange(...)} />│
          └───────────────────────────────────────────────┘

2. Wzorzec Lifting State Up (Współdzielenie stanu między rodzeństwem)

Gdy dwa komponenty siostrzane (np. CustomInput oraz MyDisplayComponent) muszą operować na tych samych danych, przenosimy stan do ich najbliższego wspólnego rodzica (MultiComponent):

// src/components/MultiComponent.tsx
import { useState } from 'react';

// 1. Komponent wyświetlający dane
interface DisplayProps {
  prefix?: string;
  message: string;
}

function MyDisplayComponent({ prefix = 'To jest', message }: DisplayProps) {
  return <h2>{prefix} {message}</h2>;
}

// 2. Komponent wejściowy (kompatybilny z wzorcem Controlled Component)
interface CustomInputProps {
  value: string;
  onValueChange: (newValue: string) => void;
}

function CustomInput({ value, onValueChange }: CustomInputProps) {
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => onValueChange(e.target.value)}
      placeholder="Własny input"
    />
  );
}

// 3. Komponent nadrzędny łączący oba komponenty potomne
export function MultiComponent() {
  const [mMsg, setMMsg] = useState<string>('komponent');

  return (
    <div>
      <CustomInput value={mMsg} onValueChange={setMMsg} />
      <MyDisplayComponent message={mMsg} />
    </div>
  );
}

3. Projekcja treści: Właściwość children (Odpowiednik Slotów z Vue)

W Vue do wstrzykiwania dowolnej treści HTML do wnętrza komponentu używamy znacznika <slot></slot>. W React wszystko, co rodzic umieści pomiędzy znacznikiem otwierającym a zamykającym komponentu (<FancyButton>Treść</FancyButton>), trafia automatycznie do specjalnego propsa o nazwie children (typowanego jako React.ReactNode).

Jeżeli komponent potrzebuje kilku „nazwanych slotów” (np. nagłówka, treści głównej i stopki w komponencie Card), przekazujemy je po prostu jako kolejne właściwości typu React.ReactNode:

// src/components/FancyButton.tsx
import type { ReactNode, ButtonHTMLAttributes } from 'react';

interface FancyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: ReactNode;
  icon?: ReactNode; // Odpowiednik nazwanego slotu (np. <slot name="icon">)
}

export function FancyButton({ children, icon, ...rest }: FancyButtonProps) {
  return (
    <button
      style={{
        background: '#42b983',
        color: 'white',
        padding: '10px 16px',
        border: 'none',
        borderRadius: '6px',
        cursor: 'pointer',
        marginRight: '8px'
      }}
      {...rest}
    >
      {icon && <span style={{ marginRight: '6px' }}>{icon}</span>}
      {children}
    </button>
  );
}

// Użycie w komponencie nadrzędnym:
export function ButtonsDemo() {
  return (
    <div>
      <FancyButton onClick={() => alert('Kliknięto!')}>
        Kliknij mnie!
      </FancyButton>

      <FancyButton icon={<span>🚀</span>}>
        <span style={{ fontWeight: 'bold' }}>Zatwierdź</span>
      </FancyButton>
    </div>
  );
}

5. Stan Lokalny, Referencje i Cykl Życia w Hookach (useState, useRef, useEffect)

Cel: Zrozumienie reguł działania Hooków, niemodyfikowalności stanu, dostępu do elementów DOM oraz odwzorowania cyklu życia komponentu za pomocą useEffect.

1. Zasady Hooków (Rules of Hooks)

Hooki (funkcje rozpoczynające się od prefiksu use*) pozwalają komponentom funkcyjnym przechowywać stan i korzystać z cyklu życia. Ponieważ React identyfikuje poszczególne wywołania useState i useEffect na podstawie kolejności ich wywołania w tablicy wewnętrznej węzła Fiber, obowiązują dwie bezwzględne reguły:

  1. Wywołuj Hooki wyłącznie na najwyższym poziomie funkcji komponentu (lub własnego Hooka) – nigdy nie umieszczaj wywołań useState czy useEffect wewnątrz instrukcji warunkowych if, pętli for ani po instrukcji wczesnego return!
  2. Wywołuj Hooki wyłącznie wewnątrz komponentów funkcyjnych Reacta lub własnych funkcji Custom Hooks (nigdy w zwykłych funkcjach pomocniczych JS).

2. Cykl życia komponentu a Hook useEffect

W nowoczesnym React komponenty funkcyjne nie posiadają oddzielnych metod klasowych takich jak mounted czy unmounted. Zamiast tego synchronizację z systemami zewnętrznymi (timerami, nasłuchiwaczami zdarzeń window, zapytaniami sieciowymi) realizujemy za pomocą Hooka useEffect(setup, dependencies?):

Montowanie komponentu (Mount)
       │
       ▼
  [ Wywołanie funkcji komponentu (Render) ]
       │
       ▼
  [ Aktualizacja DOM i Odrysowanie ekranu (Paint) ]
       │
       ▼
  [ Uruchomienie funkcji setup() w useEffect ]
       │
       ├──► Gdy zmieni się zależność z tablicy [dep1, dep2]:
       │      1. Wywołanie funkcji czyszczącej cleanup() z POPRZEDNIEGO renderu
       │      2. Wywołanie funkcji setup() z NOWYMI wartościami
       │
       └──► Gdy komponent jest usuwany z ekranu (Unmount):
              • Ostateczne wywołanie funkcji czyszczącej cleanup()

Trzy warianty tablicy zależności w useEffect:

Składnia useEffect Odpowiednik cyklu życia (np. z Vue) Kiedy uruchamia się efekt?
useEffect(() => { ... }, []) (pusta tablica []) onMounted (+ onUnmounted dla zwracanej funkcji cleanup) Tylko raz po pierwszym zamontowaniu komponentu w DOM (w StrictMode w dev: montaż → cleanup → montaż).
useEffect(() => { ... }, [a, b]) (tablica ze zmiennymi) watch([a, b], ...) + onMounted Po pierwszym zamontowaniu oraz po każdym renderze, w którym zmieniła się wartość a lub b (porównanie przez Object.is).
useEffect(() => { ... }) (brak drugiego argumentu) onMounted + onUpdated Po każdym renderowaniu komponentu (rzadko stosowane – grozi pętlą nieskończoną przy wywołaniu setState wewnątrz!).

Przykład: Obserwator zmian z czyszczeniem timera (odpowiednik watch z Vue)

// src/components/QuestionWatcher.tsx
import { useState, useEffect } from 'react';

export function QuestionWatcher() {
  const [question, setQuestion] = useState<string>('');
  const [answer, setAnswer] = useState<string>('Pytania zwykle kończą się znakiem "?" ;-)');

  useEffect(() => {
    // Reagujemy tylko wtedy, gdy wpisane pytanie zawiera znak '?'
    if (!question.includes('?')) {
      return;
    }

    setAnswer('Myślę...');

    // Symulacja asynchronicznego zapytania z opóźnieniem 1.5s
    const timerId = window.setTimeout(() => {
      setAnswer('Odpowiedź brzmi: 42.');
    }, 1500);

    // FUNKCJA CZYSZCZĄCA (Cleanup):
    // Jeśli użytkownik wpisze kolejny znak przed upływem 1.5s lub zamknie komponent,
    // anulujemy poprzedni timer, zapobiegając wyciekom pamięci i wyścigom!
    return () => {
      clearTimeout(timerId);
    };
  }, [question]); // Tablica zależności: efekt śledzi zmiany zmiennej 'question'

  return (
    <div>
      <p>
        Zadaj pytanie:{' '}
        <input
          type="text"
          value={question}
          onChange={(e) => setQuestion(e.target.value)}
        />
      </p>
      <p>{answer}</p>
    </div>
  );
}

3. Bezpośredni dostęp do elementów DOM i wartości niepowodujących renderu (useRef)

Hook useRef pełni dwie funkcje:

  1. Przechowuje mutowalną wartość (.current), której zmiana nie wywołuje ponownego renderowania komponentu (np. identyfikator timera lub poprzednia wartość stanu).
  2. Pozwala uzyskać bezpośrednią referencję do fizycznego węzła DOM (np. aby wywołać inputRef.current?.focus()).

💡 Nowość w React 19: ref jako zwykły prop (Koniec forwardRef) i funkcje czyszczące ref!
W React 18 i starszych przekazywanie ref do własnego komponentu potomnego wymagało opakowywania go w funkcję React.forwardRef(...). W React 19 ref jest dostępny jako standardowa właściwość w obiekcie props! Dodatkowo callback ref={(el) => { ... return () => cleanup(); }} obsługuje teraz zwracanie funkcji czyszczącej (co stanowi bezpośredni odpowiednik niestandardowych dyrektyw DOM app.directive z Vue!).

// src/components/AutoFocusInput.tsx (React 19)
import { useRef, type Ref } from 'react';

interface MyInputProps {
  placeholder?: string;
  ref?: Ref<HTMLInputElement>; // W React 19 'ref' to zwykły prop!
}

function MyCustomInput({ placeholder, ref }: MyInputProps) {
  return <input ref={ref} placeholder={placeholder} />;
}

export function FocusDemo() {
  const inputElementRef = useRef<HTMLInputElement>(null);

  // Callback ref jako odpowiednik dyrektywy v-random-color z Vue:
  const randomColorRef = (el: HTMLHeadingElement | null) => {
    if (el) {
      el.style.color = `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`;
    }
  };

  return (
    <div>
      <h2 ref={randomColorRef}>Nagłówek z losowym kolorem</h2>
      <MyCustomInput ref={inputElementRef} placeholder="Kliknij przycisk, aby ustawić fokus" />
      <button onClick={() => inputElementRef.current?.focus()}>
        Ustaw fokus na polu
      </button>
    </div>
  );
}

6. Wartości Pochodne (useMemo), Własne Hooki (Custom Hooks) i Nowe Hooki React 19

Cel: Tworzenie buforowanych wartości pochodnych, wyodrębnianie reużywalnej logiki stanowej do własnych Hooków (useMousePosition, useFetch) oraz poznanie nowych mechanizmów React 19 (useActionState, useOptimistic, use).

1. Wartości pochodne i useMemo (Odpowiednik computed z Vue)

W React proste wartości pochodne (np. połączenie firstName i lastName) obliczamy bezpośrednio w ciele funkcji komponentu podczas renderowania. Jeżeli jednak obliczenie jest kosztowne procesorowo (np. filtrowanie i sortowanie tablicy 50 000 rekordów), opakowujemy je w Hook useMemo(() => wynik, [zależności]), który buforuje (cachuje) wynik i przelicza go tylko wtedy, gdy zmieni się jedna z zależności:

// src/components/ComputedDemo.tsx
import { useState, useMemo } from 'react';

export function ComputedDemo() {
  const [firstName, setFirstName] = useState<string>('Jan');
  const [lastName, setLastName] = useState<string>('Kowalski');
  const [unrelatedCounter, setUnrelatedCounter] = useState<number>(0);

  // Odpowiednik computed property: przeliczy się tylko przy zmianie firstName lub lastName!
  const fullName = useMemo(() => {
    console.log('Obliczanie fullName...');
    return `${firstName} ${lastName}`;
  }, [firstName, lastName]);

  return (
    <div>
      <input value={firstName} onChange={e => setFirstName(e.target.value)} placeholder="Imię" />
      <input value={lastName} onChange={e => setLastName(e.target.value)} placeholder="Nazwisko" />
      <p>Pełne imię: <strong>{fullName}</strong></p>

      <button onClick={() => setUnrelatedCounter(c => c + 1)}>
        Niezwiązany licznik ({unrelatedCounter}) – nie wyzwoli przeliczenia fullName
      </button>
    </div>
  );
}

2. Tworzenie własnych Hooków (Custom Hooks – Odpowiednik Composables w Vue)

Największą siłą architektury Hooków jest możliwość wyodrębnienia logiki stanowej i cyklu życia do osobnej funkcji wielokrotnego użytku (Custom Hook). Zgodnie z konwencją oraz wymogami lintera ESLint, nazwa takiej funkcji musi rozpoczynać się od prefiksu use (np. useMousePosition, useFetch). Każde wywołanie własnego Hooka w osobnym komponencie posiada całkowicie odizolowany, własny stan.

Przykład A: Hook useMousePosition (Śledzenie pozycji kursora myszy)
// src/hooks/useMousePosition.ts
import { useState, useEffect } from 'react';

export function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMouseMove(event: MouseEvent) {
      setPosition({ x: event.pageX, y: event.pageY });
    }

    window.addEventListener('mousemove', handleMouseMove);
    // Automatyczne odpięcie nasłuchiwacza przy odmontowaniu komponentu:
    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  return position; // Zwraca { x, y }
}

Użycie w dowolnym komponencie:

import { useMousePosition } from '../hooks/useMousePosition';

export function MouseTracker() {
  const { x, y } = useMousePosition();
  return <p>Pozycja myszy to: {x}, {y}</p>;
}
Przykład B: Generyczny Hook useFetch<T> z obsługą AbortController

Podczas ręcznego pobierania danych w useEffect kluczowe jest użycie obiektu AbortController, aby przerwać trwające zapytanie HTTP, gdy komponent zostanie odmontowany lub zmieni się adres url (zapobiega to błędowi Race Condition):

// src/hooks/useFetch.ts
import { useState, useEffect, useCallback } from 'react';

interface UseFetchResult<T> {
  data: T | null;
  error: Error | null;
  loading: boolean;
  refetch: () => void;
}

export function useFetch<T = unknown>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [reloadTrigger, setReloadTrigger] = useState<number>(0);

  const refetch = useCallback(() => {
    setReloadTrigger(prev => prev + 1);
  }, []);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`Błąd HTTP: ${response.status} ${response.statusText}`);
        }
        const json = (await response.json()) as T;
        setData(json);
      } catch (err) {
        if (err instanceof DOMException && err.name === 'AbortError') {
          return; // Ignorujemy celowe anulowanie zapytania
        }
        setError(err instanceof Error ? err : new Error(String(err)));
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    fetchData();

    // Anulowanie trwającego żądania przy zmianie URL lub odmontowaniu komponentu:
    return () => controller.abort();
  }, [url, reloadTrigger]);

  return { data, error, loading, refetch };
}

3. Nowe Hooki i API w React 19: useActionState, useOptimistic oraz use()

React 19 wprowadził natywne wsparcie dla asynchronicznych Akcji (Actions) w formularzach oraz odczytywania zasobów:


7. Nawigacja w Aplikacjach SPA: React Router v7 vs TanStack Router

Cel: Budowa wielowidokowych aplikacji SPA (Single-Page Application) przy użyciu klasycznego react-router oraz nowoczesnego, w 100% bezpiecznego typowo TanStack Router.

W aplikacjach SPA nawigacja pomiędzy podstronami odbywa się po stronie przeglądarki za pomocą HTML5 History API (pushState / replaceState), bez pełnego przeładowywania dokumentu z serwera. W ekosystemie React dominują dwa rozwiązania:

1. Wariant A: Klasyczny routing deklaratywny w react-router (v7)

Instalacja pakietu:

npm install react-router

Konfiguracja tras, linków nawigacyjnych (<Link>, <NavLink>) oraz miejsca renderowania podstron (<Routes> / <Outlet>):

// src/AppRouterDemo.tsx
import { BrowserRouter, Routes, Route, NavLink, useParams } from 'react-router';

function HomeView() {
  return <div><h2>Strona Główna (Home)</h2></div>;
}

function AboutView() {
  return <div><h2>O projekcie (About)</h2></div>;
}

function UserProfileView() {
  // Odczyt dynamicznego parametru :id ze ścieżki URL
  const { id } = useParams<{ id: string }>();
  return <div><h2>Profil użytkownika o ID: {id}</h2></div>;
}

export default function AppRouterDemo() {
  return (
    <BrowserRouter>
      <header>
        <h1>Hello React SPA!</h1>
        <nav style={{ display: 'flex', gap: '12px', marginBottom: '20px' }}>
          <NavLink to="/">Home</NavLink>
          <NavLink to="/about">About</NavLink>
          <NavLink to="/users/42">Użytkownik #42</NavLink>
        </nav>
      </header>

      <main>
        <Routes>
          <Route path="/" element={<HomeView />} />
          <Route path="/about" element={<AboutView />} />
          <Route path="/users/:id" element={<UserProfileView />} />
          <Route path="*" element={<h2>404 – Nie znaleziono strony</h2>} />
        </Routes>
      </main>
    </BrowserRouter>
  );
}

2. Wariant B: Nowoczesny, w 100% bezpieczny typowo TanStack Router (File-Based Routing)

W klasycznym react-router ścieżki przekazywane do <Link to="/users/42"> są zwykłymi ciągami znaków (string). Literówka w adresie URL nie zostanie wykryta przez kompilator TypeScript.

TanStack Router (@tanstack/react-router) wprowadza pełne bezpieczeństwo typów (100% Type-Safety) dla ścieżek URL, parametrów dynamicznych oraz parametrów zapytania (Search Params z walidacją schematów np. przez bibliotekę zod), a także wtyczkę Vite automatycznie generującą drzewo tras na podstawie struktury plików w katalogu src/routes/.

Instalacja TanStack Router oraz wtyczki Vite:

npm install @tanstack/react-router
npm install -D @tanstack/router-plugin @tanstack/router-devtools
  1. Rejestracja wtyczki generującej trasy w vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';

export default defineConfig({
  plugins: [
    TanStackRouterVite(), // MUSI znajdować się przed wtyczką react()!
    react(),
  ],
});
  1. Struktura plików w katalogu src/routes/:
    • src/routes/__root.tsx – główny układ aplikacji (zawiera nawigację oraz <Outlet/>).
    • src/routes/index.tsx – strona główna pod ścieżką /.
    • src/routes/users.$userId.tsx – trasa dynamiczna /users/:userId.
// src/routes/users.$userId.tsx
import { createFileRoute, Link } from '@tanstack/react-router';

export const Route = createFileRoute('/users/$userId')({
  component: UserComponent,
});

function UserComponent() {
  // Pełne wnioskowanie typów! TypeScript wie, że zwracany obiekt ma właściwość { userId: string }
  const { userId } = Route.useParams();

  return (
    <div>
      <h2>Profil użytkownika: {userId}</h2>
      {/* Kompilator TS zgłosi błąd, jeśli pomylimy nazwę trasy lub pominiemy parametr userId! */}
      <Link to="/users/$userId" params={{ userId: '99' }}>
        Przejdź do użytkownika #99
      </Link>
    </div>
  );
}

8. Zarządzanie Stanem Serwera (TanStack Query v5) oraz Tabele Danych (TanStack Table)

Cel: Oddzielenie stanu asynchronicznego API od stanu lokalnego za pomocą @tanstack/react-query oraz budowa interaktywnych tabel danych z sortowaniem, filtrowaniem i paginacją w @tanstack/react-table.

1. Zarządzanie stanem serwera z @tanstack/react-query

Pobieranie danych z REST API wewnątrz useEffect wymaga ręcznego pisania logiki pamięci podręcznej (caching), deduplikacji równoczesnych zapytań, ponawiania prób po błędzie (retries) oraz odświeżania danych. Biblioteka TanStack Query (@tanstack/react-query) automatyzuje wszystkie te procesy.

Instalacja:

npm install @tanstack/react-query @tanstack/react-query-devtools
  1. Konfiguracja QueryClientProvider w src/main.tsx:
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import App from './App.tsx';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // Dane są uznawane za świeże przez 1 minutę
      retry: 2,             // Dwukrotne ponowienie zapytania w razie błędu sieci
    },
  },
});

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  </StrictMode>
);
  1. Pobieranie danych (useQuery) oraz modyfikacja z inwalidacją cache (useMutation):
// src/components/TodoServerList.tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

interface TodoItem {
  id: number;
  title: string;
  completed: boolean;
}

export function TodoServerList() {
  const queryClient = useQueryClient();

  // 1. Pobieranie i buforowanie listy zadań pod kluczem ['todos']
  const { data: todos, isLoading, isError, error } = useQuery<TodoItem[]>({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
      if (!res.ok) throw new Error('Błąd pobierania zadań z API');
      return res.json();
    },
  });

  // 2. Mutacja (POST) z automatycznym unieważnieniem cache po sukcesie
  const addTodoMutation = useMutation({
    mutationFn: async (newTitle: string) => {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: newTitle, completed: false }),
      });
      return res.json();
    },
    onSuccess: () => {
      // Wymusza automatyczne pobranie świeżej listy ['todos'] z serwera!
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  if (isLoading) return <p>Ładowanie danych z serwera...</p>;
  if (isError) return <p style={{ color: 'red' }}>Błąd: {error.message}</p>;

  return (
    <div>
      <button
        disabled={addTodoMutation.isPending}
        onClick={() => addTodoMutation.mutate('Nowe zadanie laboratoryjne')}
      >
        {addTodoMutation.isPending ? 'Zapisywanie...' : 'Dodaj zadanie'}
      </button>

      <ul>
        {todos?.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

2. Zaawansowane tabele danych: @tanstack/react-table (Odpowiednik vue-good-table-next)

W ekosystemie React de facto standardem do budowy wydajnych tabel obsługujących sortowanie, filtrowanie i paginację jest TanStack Table (@tanstack/react-table). Jest to biblioteka typu Headless UI – dostarcza kompletną logikę oraz stan tabeli, pozostawiając pełną kontrolę nad znacznikami HTML i stylami CSS (np. Tailwind CSS).

Instalacja:

npm install @tanstack/react-table

Przykład tabeli kontaktów z sortowaniem kolumn po kliknięciu w nagłówek:

// src/components/ContactsTable.tsx
import { useState, useMemo } from 'react';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  flexRender,
  type ColumnDef,
  type SortingState,
} from '@tanstack/react-table';
import { useContactsStore, type Contact } from '../stores/contactsStore';

export function ContactsTable() {
  const { contactsMode, contactsList } = useContactsStore();
  const [sorting, setSorting] = useState<SortingState>([]);

  const columns = useMemo<ColumnDef<Contact>[]>(
    () => [
      { accessorKey: 'name', header: 'Imię i nazwisko' },
      { accessorKey: 'phone', header: 'Telefon' },
    ],
    []
  );

  const table = useReactTable({
    data: contactsList,
    columns,
    state: { sorting },
    onSortingChange: setSorting,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  if (!contactsMode) return null;

  return (
    <div className="content">
      <h2>Lista kontaktów:</h2>
      <table border={1} cellPadding={8} style={{ borderCollapse: 'collapse', width: '100%' }}>
        <thead>
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  onClick={header.column.getToggleSortingHandler()}
                  style={{ cursor: 'pointer', userSelect: 'none' }}
                >
                  {flexRender(header.column.columnDef.header, header.getContext())}
                  {{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? null}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map(row => (
            <tr key={row.id}>
              {row.getVisibleCells().map(cell => (
                <td key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

9. Globalny Stan Aplikacji: Context API vs Zustand (Odpowiednik Pinia)

Cel: Zarządzanie stanem współdzielonym w dużych aplikacjach zgodnie ze wzorcem Single Source of Truth (SSOT) przy użyciu lekkiej biblioteki Zustand.

Gdy stan aplikacji musi być dostępny w wielu odległych miejscach drzewa komponentów, przekazywanie go przez dziesiątki pośrednich komponentów za pomocą props prowadzi do antywzorca Prop Drilling.

1. Porównanie rozwiązań zarządzania stanem w React

Rozwiązanie Charakterystyka Kiedy stosować?
React Context API (createContext + use) Wbudowane w React. Uwaga: Zmiana wartości w kontekście powoduje re-render wszystkich komponentów konsumujących ten kontekst! Dane rzadko zmieniające się globalnie: motyw kolorystyczny (Dark/Light), wybrany język (i18n), obiekt zalogowanego użytkownika.
Zustand Minimalistyczny, niezwykle szybki magazyn stanu oparty na Hookach i selektorach. Nie wymaga opakowywania aplikacji w <Provider>! Zalecany standard dla średnich i dużych aplikacji SPA (bezpośredni odpowiednik prostoty i ergonomii Pinia z Vue).
Redux Toolkit (RTK) Rozbudowany wzorzec Flux z akcjami, reducerami i middleware. Bardzo duże aplikacje korporacyjne ze złożonymi przepływami zdarzeń.

2. Zarządzanie stanem za pomocą biblioteki Zustand

Instalacja:

npm install zustand

Magazyn (Store) w Zustand tworzymy za pomocą funkcji create<T>(). Łączy on w jednym obiekcie:

// src/stores/counterStore.ts
import { create } from 'zustand';

export interface UserInfo {
  name: string;
  age: number;
}

interface CounterState {
  count: number;
  user: UserInfo | null;
  // Akcje:
  increment: () => void;
  randomizeCounter: () => void;
  setUser: (user: UserInfo | null) => void;
}

export const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  user: null,

  increment: () => set((state) => ({ count: state.count + 1 })),

  randomizeCounter: () =>
    set({ count: Math.round(100 * Math.random()) }),

  setUser: (user) => set({ user }),
}));

Magazyn kontaktów (contactsStore.ts) wykorzystany we wcześniejszym przykładzie tabeli:

// src/stores/contactsStore.ts
import { create } from 'zustand';

export interface Contact {
  name: string;
  phone: string;
}

interface ContactsStore {
  contactsMode: boolean;
  contactsList: Contact[];
  toggleMode: () => void;
}

export const useContactsStore = create<ContactsStore>((set) => ({
  contactsMode: true,
  contactsList: [
    { name: 'Jan Kowalski', phone: '+48 500 100 200' },
    { name: 'Anna Nowak', phone: '+48 600 300 400' },
  ],
  toggleMode: () => set((state) => ({ contactsMode: !state.contactsMode })),
}));

3. Użycie magazynu Zustand i selektorów w komponencie

Aby komponent odświeżał się wyłącznie wtedy, gdy zmieni się konkretny wycinek stanu, przekazujemy do Hooka funkcję selektora state => state.count:

// src/components/CounterWidget.tsx
import { useCounterStore } from '../stores/counterStore';

export function CounterWidget() {
  // Selektory gwarantują, że komponent zrerenderuje się TYLKO przy zmianie 'count'
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const randomizeCounter = useCounterStore((state) => state.randomizeCounter);

  // Wartość pochodna (Getter):
  const doubleCount = count * 2;
  const doublePlusOne = doubleCount + 1;

  return (
    <div>
      <p>Count: {count} | Double: {doubleCount} | Double+1: {doublePlusOne}</p>
      <button onClick={increment}>+1</button>
      <button onClick={randomizeCounter}>Randomize</button>
    </div>
  );
}

10. Stylowanie Interfejsu: CSS Modules oraz Tailwind CSS (v4 / v3)

Cel: Hermetyzacja stylów komponentów za pomocą CSS Modules oraz konfiguracja frameworka Tailwind CSS w projekcie React + Vite.

W przeciwieństwie do plików .vue posiadających atrybut <style scoped>, standardowy import import './App.css' w React ładuje style do zasięgu globalnego. Aby uniknąć kolizji nazw klas w dużych aplikacjach, stosujemy CSS Modules lub Tailwind CSS.

1. Lokalny zasięg stylów: CSS Modules (*.module.css)

Vite obsługuje pliki z sufiksem .module.css natywnie bez żadnej dodatkowej konfiguracji. Podczas kompilacji nazwy klas są automatycznie hashowane (np. .button → ._button_1a2b3_1):

/* src/components/Alert.module.css */
.box {
  padding: 12px;
  border-radius: 8px;
  background-color: #eef6ff;
}
// src/components/Alert.tsx
import styles from './Alert.module.css';

export function Alert({ message }: { message: string }) {
  return <div className={styles.box}>{message}</div>;
}

2. Konfiguracja Tailwind CSS v4 (Z natywną wtyczką @tailwindcss/vite)

Tailwind CSS pozwala budować nowoczesne interfejsy bezpośrednio w kodzie JSX/TSX za pomocą klas narzędziowych (utility-first). W najnowszej wersji Tailwind CSS v4 konfiguracja w projekcie Vite została maksymalnie uproszczona (nie wymaga już plików postcss.config.js ani tailwind.config.js):

  1. Instalacja pakietów Tailwind CSS v4 oraz narzędzi do warunkowego łączenia klas (clsx + tailwind-merge):
npm install tailwindcss @tailwindcss/vite clsx tailwind-merge
  1. Dodanie wtyczki w vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
});
  1. Import w pliku ./src/index.css:
@import "tailwindcss";

Konfiguracja dla projektów ze starszym Tailwind CSS v3 (PostCSS): W starszych projektach opartych na Tailwind CSS v3 instalujemy npm install -D tailwindcss@3 postcss autoprefixer, inicjujemy konfigurację npx tailwindcss init -p, w pliku tailwind.config.js ustawiamy ścieżki:

export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: { extend: {} },
  plugins: [],
}

a w ./src/index.css umieszczamy dyrektywy @tailwind base; @tailwind components; @tailwind utilities;.


11. Diagnostyka i Debugowanie Aplikacji React (React DevTools, Profiler i VS Code)

Cel: Opanowanie narzędzi inspekcji drzewa komponentów, profilowania wydajności renderowania oraz debugowania kodu TypeScript w edytorze Visual Studio Code.

1. Rozszerzenie przeglądarki React Developer Tools

Do przeglądarek opartych na silniku Chromium (Chrome, Edge) oraz Firefox instalujemy oficjalne rozszerzenie React Developer Tools, które dodaje do narzędzi deweloperskich (F12) dwie nowe zakładki:

2. Konfiguracja debuggera w Visual Studio Code (.vscode/launch.json)

VS Code pozwala stawiać punkty wstrzymania (Breakpoints – klawisz F9) bezpośrednio w kodzie źródłowym .tsx. Dzięki mapom źródłowym (Source Maps) generowanym przez Vite, debugger zatrzymuje wykonanie dokładnie na wybranej linijce TypeScriptu.

Aby jednym klawiszem F5 uruchomić równocześnie serwer deweloperski Vite w terminalu oraz sesję debugowania przeglądarki Edge/Chrome, tworzymy plik .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "msedge",
      "request": "launch",
      "name": "localhost (Edge)",
      "url": "http://localhost:5173",
      "webRoot": "${workspaceFolder}"
    },
    {
      "command": "npm run dev",
      "name": "Vite Dev Server",
      "request": "launch",
      "type": "node-terminal",
      "cwd": "${workspaceFolder}"
    }
  ],
  "compounds": [
    {
      "name": "Launch React App in Edge",
      "configurations": ["Vite Dev Server", "localhost (Edge)"],
      "stopAll": true
    }
  ]
}

3. Własny Hook useDebugValue

Tworząc własne Hooki biblioteczne (Custom Hooks), możemy użyć wbudowanego Hooka useDebugValue(value), aby wyświetlić czytelną etykietę diagnostyczną obok nazwy naszego Hooka w oknie React DevTools.


12. Strategia Testowania Aplikacji React: Vitest, React Testing Library oraz Playwright (Page Object Model)

Cel: Kompleksowe testowanie aplikacji React na trzech poziomach: testy jednostkowe funkcji i Hooków (Vitest), testy integracyjne komponentów (React Testing Library) oraz testy End-to-End w prawdziwej przeglądarce (Playwright + wzorzec Page Object Model).

                    /\
                   /  \      3. Testy End-to-End (E2E) — Playwright (Page Object Model)
                  /────\        Pełna aplikacja w przeglądarce Chromium/Firefox/WebKit
                 /      \
                /────────\   2. Testy Komponentów — Vitest + React Testing Library
               /          \     Renderowanie komponentów, interakcje użytkownika (userEvent)
              /────────────\
             /              \ 1. Testy Jednostkowe — Vitest + renderHook
            /________________\   Czyste funkcje biznesowe TS, Custom Hooks, Store Zustand

1. Instalacja i konfiguracja środowiska Vitest + React Testing Library

Vitest korzysta z tego samego potoku konfiguracyjnego co Vite, dzięki czemu natychmiast rozumie składnię TSX i aliasy ścieżek. React Testing Library (@testing-library/react) promuje testowanie komponentów z perspektywy rzeczywistego użytkownika (wyszukiwanie elementów po rolach dostępności ARIA getByRole, etykietach getByLabelText czy widocznym tekście getByText, zamiast zaglądania w wewnętrzną strukturę klas CSS).

Instalacja pakietów testowych:

npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event

Konfiguracja w vite.config.ts:

/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test/setup.ts',
  },
});

W pliku ./src/test/setup.ts importujemy rozszerzone asercje DOM (takie jak toBeInTheDocument()):

// src/test/setup.ts
import '@testing-library/jest-dom/vitest';

2. Poziom 1: Testy jednostkowe funkcji oraz własnych Hooków (renderHook)

Czyste funkcje oraz własne Hooki testujemy bez konieczności budowania pełnego interfejsu graficznego. Do przetestowania własnego Hooka wykorzystujemy funkcję pomocniczą renderHook oraz act z @testing-library/react:

// src/hooks/useCounter.ts
import { useState, useCallback } from 'react';

export function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = useCallback(() => setCount(c => c + 1), []);
  return { count, increment };
}
// src/hooks/useCounter.test.ts
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  it('inicjalizuje licznik wartością 0 i zwiększa go o 1 po wywołaniu increment()', () => {
    const { result } = renderHook(() => useCounter(0));

    expect(result.current.count).toBe(0);

    // Wszystkie akcje zmieniające stan Reacta w teście Hooka opakowujemy w act():
    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });
});

3. Poziom 2: Testy komponentów w React Testing Library (render, screen, userEvent)

Testy komponentów sprawdzają poprawność renderowania propsów oraz reakcję komponentu na zdarzenia użytkownika (kliknięcia, wpisywanie tekstu z klawiatury za pomocą @testing-library/user-event):

// src/components/WeatherHeader.tsx
interface WeatherHeaderProps {
  title: string;
}

export function WeatherHeader({ title }: WeatherHeaderProps) {
  return (
    <header>
      <h1>{title}</h1>
    </header>
  );
}
// src/components/WeatherHeader.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { WeatherHeader } from './WeatherHeader';

describe('WeatherHeader.tsx Test', () => {
  it('renderuje przekazany tytuł w nagłówku h1', () => {
    // 1. Zamontowanie komponentu w wirtualnym DOM (jsdom)
    render(<WeatherHeader title="React Project" />);

    // 2. Wyszukanie nagłówka po jego roli semantycznej ARIA i weryfikacja tekstu
    const heading = screen.getByRole('heading', { level: 1 });
    expect(heading).toHaveTextContent(/React Project/i);
  });
});

4. Poziom 3: Testy End-to-End (E2E) w Playwright i Wzorzec Page Object Model (POM)

Testy End-to-End (E2E) uruchamiają kompletną, zbudowaną aplikację w prawdziwych silnikach przeglądarek (Chromium, Firefox oraz WebKit) i symulują rzeczywiste zachowanie użytkownika. Rekomendowanym obecnie narzędziem E2E jest Playwright (rozwijany przez Microsoft), wyróżniający się wbudowanym mechanizmem Auto-Waiting (automatycznym oczekiwaniem, aż element stanie się widoczny i klikalny), obsługą wielu kart jednocześnie oraz narzędziem Trace Viewer.

Instalacja i inicjalizacja Playwright w projekcie:

npm init playwright@latest
Wzorzec Architektoniczny Page Object Model (Model Obiektu Strony – POM)

W dużych zestawach testów E2E bezpośrednie wpisywanie selektorów (page.getByTestId(...), page.locator(...)) wewnątrz dziesiątków plików testowych *.spec.ts prowadzi do duplikacji kodu. Gdy w komponencie React zmieni się tekst przycisku lub struktura formularza, trzeba ręcznie naprawiać każdy test z osobna.

Wzorzec Page Object Model (POM) rozwiązuje ten problem poprzez hermetyzację struktury strony w dedykowanej klasie TypeScript:

┌───────────────────────────────────────────┐         ┌───────────────────────────────────────────┐
│        Plik testu: e2e/todo.spec.ts       │         │     Klasa POM: e2e/pages/TodoPage.ts      │
│                                           │         │                                           │
│  test('dodaje zadanie', async ({page}) => │         │  readonly input: Locator;                 │
│    const todoPage = new TodoPage(page);   │────────►│  readonly addButton: Locator;             │
│    await todoPage.goto();                 │ wywołuje│  readonly todoItems: Locator;             │
│    await todoPage.addTodo('Kupić kawę');  │ metody  │                                           │
│    await expect(todoPage.todoItems)       │         │  async addTodo(text: string) {            │
│      .toHaveCount(1);                     │         │    await this.input.fill(text);           │
│  });                                      │         │    await this.addButton.click();          │
└───────────────────────────────────────────┘         │  }                                        │
                                                      └─────────────────────┬─────────────────────┘
                                                                            │ steruje przez CDP
                                                                            ▼
                                                      ┌───────────────────────────────────────────┐
                                                      │   Przeglądarka (Chromium / Firefox / WK)  │
                                                      └───────────────────────────────────────────┘
1. Implementacja klasy Page Object (e2e/pages/TodoPage.ts)
// e2e/pages/TodoPage.ts
import { type Page, type Locator } from '@playwright/test';

export class TodoPage {
  readonly page: Page;
  readonly newTodoInput: Locator;
  readonly submitButton: Locator;
  readonly todoItems: Locator;

  constructor(page: Page) {
    this.page = page;
    // Preferujemy odporne na zmiany stylów lokatory semantyczne (Role / Placeholder / Label):
    this.newTodoInput = page.getByPlaceholder('Co jest do zrobienia?');
    this.submitButton = page.getByRole('button', { name: 'Dodaj' });
    this.todoItems = page.getByTestId('todo-item');
  }

  async goto() {
    await this.page.goto('http://localhost:5173/');
  }

  async addTodo(title: string) {
    await this.newTodoInput.fill(title);
    await this.submitButton.click();
  }

  async toggleTodoByText(title: string) {
    const item = this.todoItems.filter({ hasText: title });
    await item.getByRole('checkbox').check();
  }
}
2. Implementacja testu E2E wykorzystującego Page Object (e2e/todo.spec.ts)
// e2e/todo.spec.ts
import { test, expect } from '@playwright/test';
import { TodoPage } from './pages/TodoPage';

test.describe('Zarządzanie listą zadań w aplikacji React', () => {
  test('użytkownik może dodać nowe zadanie i oznaczyć je jako ukończone', async ({ page }) => {
    const todoPage = new TodoPage(page);

    // 1. Nawigacja do strony głównej aplikacji
    await todoPage.goto();

    // 2. Wykonanie akcji biznesowych za pośrednictwem modelu POM
    await todoPage.addTodo('Przygotować sprawozdanie z laboratorium React');
    await todoPage.addTodo('Skonfigurować testy Playwright');

    // 3. Weryfikacja stanu interfejsu (Auto-Retrying Assertions)
    await expect(todoPage.todoItems).toHaveCount(2);
    await expect(todoPage.todoItems.first()).toContainText('Przygotować sprawozdanie');

    // 4. Oznaczenie pierwszego zadania jako wykonane
    await todoPage.toggleTodoByText('Przygotować sprawozdanie z laboratorium React');
    const firstCheckbox = todoPage.todoItems.first().getByRole('checkbox');
    await expect(firstCheckbox).toBeChecked();
  });
});

Uruchamianie testów Playwright z podglądem interaktywnym (UI Mode):

# Uruchomienie testów w trybie konsolowym (headless):
npx playwright test

# Uruchomienie interaktywnego okna Playwright UI Mode z podglądem krok po kroku (Time Travel):
npx playwright test --ui