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:
- Programista deklaruje jedynie, jak widok powinien wyglądać dla danego stanu danych.
- Gdy stan ulega zmianie, React ponownie wywołuje funkcję komponentu, porównuje zwróconą strukturę z poprzednią wersją i samodzielnie wylicza minimalny zestaw operacji DOM, które należy wykonać.
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). │
└─────────────────────────────────────────────────────────────────────────┘
- Dlaczego dwie fazy są kluczowe? Ponieważ Faza Renderowania operuje wyłącznie na lekkich obiektach JavaScript w pamięci RAM, silnik React Fiber może ją wstrzymać, podzielić na mniejsze fragmenty czasu (Time Slicing) lub porzucić, jeśli w międzyczasie użytkownik wpisze znak z klawiatury (zadanie o wyższym priorytecie). Dopiero Faza Zatwierdzania modyfikuje rzeczywisty DOM w jednym, atomowym kroku, zapobiegając niepłynnemu migotaniu interfejsu.
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ówuseMemo,useCallbackorazReact.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):
nvm install lts(lubnvm install latest) – pobranie i instalacja najnowszej wersji LTS / Current.nvm ls– wyświetlenie listy zainstalowanych wersji oraz wskazanie aktualnie aktywnej.nvm use <wersja>(np.nvm use 22.14.0) – przełączenie aktywnej wersji Node.js.
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 komendanpx 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:
- 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<> ... </>). - Nazwy atrybutów w notacji
camelCase: Ponieważ kod JSX kompiluje się do właściwości obiektów DOM w JavaScript, zamiast atrybutu HTMLclassużywamyclassName, a zamiastforw etykietach<label>używamyhtmlFor. - Obowiązkowe zamykanie wszystkich znaczników: Nawet
pojedyncze znaczniki HTML muszą być samozamykające, np.
<input />,<img src="..." alt="..." />,<br />. - 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 funkcjiMath.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
keyw metodzie.map()jest obowiązkowy i dlaczego nie wolno używać w nim indeksu tablicyindexprzy dynamicznych listach?
Arybutkeypozwala algorytmowi Diffingu (Reconciliation) jednoznacznie powiązać element z poprzedniego renderowania z elementem po zmianie tablicy. Jeśli jakokeypodasz 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ę o1. React pomyśli, że pod kluczem0zmieniła się zawartość i może błędnie zachować stan wewnętrzny kontrolek (np. fokus lub tekst w<input>) przy niewłaściwym wierszu! Jakokeyzawsze 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.
- Niemodyfikowalność
props: Komponent potomny traktuje otrzymanepropsjako dane tylko do odczytu (read-only) – nigdy nie może ich bezpośrednio modyfikować! - Komunikacja w górę drzewa (Potomek → Rodzic): Aby
komponent potomny mógł poinformować rodzica o zmianie (np. wpisaniu
tekstu w polu
<input>), rodzic przekazuje do potomka jako prop funkcję zwrotną (callback, np.onUpdatelubonChange), którą potomek wywołuje z nową wartością.
┌───────────────────────────────────────────────┐
│ 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:
- Wywołuj Hooki wyłącznie na najwyższym poziomie funkcji
komponentu (lub własnego Hooka) – nigdy nie
umieszczaj wywołań
useStateczyuseEffectwewnątrz instrukcji warunkowychif, pętliforani po instrukcji wczesnegoreturn! - 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:
- Przechowuje mutowalną wartość (
.current), której zmiana nie wywołuje ponownego renderowania komponentu (np. identyfikator timera lub poprzednia wartość stanu). - Pozwala uzyskać bezpośrednią referencję do fizycznego węzła DOM (np.
aby wywołać
inputRef.current?.focus()).
💡 Nowość w React 19:
refjako zwykły prop (KoniecforwardRef) i funkcje czyszcząceref!
W React 18 i starszych przekazywanierefdo własnego komponentu potomnego wymagało opakowywania go w funkcjęReact.forwardRef(...). W React 19refjest dostępny jako standardowa właściwość w obiekcieprops! Dodatkowo callbackref={(el) => { ... return () => cleanup(); }}obsługuje teraz zwracanie funkcji czyszczącej (co stanowi bezpośredni odpowiednik niestandardowych dyrektyw DOMapp.directivez 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:
useActionState(actionFn, initialState): Automatycznie zarządza stanem wysyłania formularza (<form action={formAction}>), zwracając aktualny stan odpowiedzi, funkcję akcji oraz flagęisPending(eliminuje konieczność ręcznego tworzenia trzech osobnychuseStatedlaloading,erroridata).useOptimistic(state, updateFn): Pozwala natychmiast wyświetlić użytkownikowi optymistycznie zaktualizowany stan UI (np. nowo dodany komentarz na liście) jeszcze zanim serwer HTTP zakończy przetwarzanie żądaniaPOST.- API
use(resource): Nowa funkcja pozwalająca odczytywać wartość bezpośrednio z obiektuPromise(w połączeniu z komponentem<Suspense fallback="{...}">) lub zContext. W przeciwieństwie do zwykłych Hooków, funkcjęuse()można wywoływać wewnątrz instrukcji warunkowychiforaz pętli!
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
- 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(),
],
});
- 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
- Konfiguracja
QueryClientProviderwsrc/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>
);
- 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:
- Stan (State) – właściwości przechowujące
dane (odpowiednik
statew Pinia), - Akcje (Actions) – funkcje modyfikujące
stan za pomocą niemodyfikowalnego wywołania
set(...)(odpowiednikactionsw Pinia).
// 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):
- Instalacja pakietów Tailwind CSS v4 oraz narzędzi do
warunkowego łączenia klas (
clsx+tailwind-merge):
npm install tailwindcss @tailwindcss/vite clsx tailwind-merge
- 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(),
],
});
- 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:
- ⚛️ Components:
- Pozwala przeglądać hierarchię komponentów React (z pominięciem szumu ze znaczników HTML).
- Umożliwia podgląd i edycję na żywo wartości
propsoraz stanu poszczególnych Hooków (State,Ref,Memo,Context). - W ustawieniach zakładki (Ikona koła zębatego → General) warto zaznaczyć opcję „Highlight updates when components render” – przeglądarka będzie wówczas obrysowywać kolorową ramką każdy komponent, który ulega przeliczeniu, co pozwala błyskawicznie wykryć zbędne re-rendery!
- ⚛️ Profiler:
- Pozwala nagrać sesję interakcji z aplikacją i wygenerować wykres płomieniowy (Flamegraph) pokazujący czas renderowania każdego komponentu w milisekundach oraz dokładną przyczynę jego odświeżenia („Why did this render?”).
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:
- Klasa Strony (
Page Object, np.TodoPage.ts): Przechowuje wszystkie lokatory (Locator) elementów danej podstrony oraz wystawia wysokopoziomowe metody biznesowe (np.addTodo(title),toggleTodo(index)). Nie zawiera asercjiexpect! - Specyfikacja Testu (
*.spec.ts): Korzysta wyłącznie z metod klasyPage Objecti wykonuje sprawdzenia asercjiexpect(...).
┌───────────────────────────────────────────┐ ┌───────────────────────────────────────────┐
│ 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