Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz
2026-02-03
Wprowadzenie
W poprzednim wpisie stworzyliśmy nasze środowisko pracy, zainstalowaliśmy Vite i uruchomiliśmy pierwszy projekt React + TypeScript. Widzieliśmy już pierwszy komponent App.tsx, ale nie zagłębialiśmy się w szczegóły. Teraz nadszedł czas, aby zrozumieć jak faktycznie działa React.
W tym wpisie skupimy się na fundamentach, bez których nie da się iść dalej: poznamy TSX (TypeScript + JSX), nauczymy się tworzyć komponenty funkcyjne, zrozumiemy jak przekazywać dane między komponentami przez props i odkryjemy moc kompozycji komponentów. To będzie wpis pełen kodu i praktycznych przykładów.
Załóżmy, że mamy już uruchomiony projekt z poprzedniego wpisu. Jeśli nie – wróć do wpisu pierwszego i wykonaj kroki instalacji. Gotowi? Zaczynamy!
Struktura projektu React – bliższe spojrzenie
Zanim zaczniemy tworzyć komponenty, poświęćmy chwilę na zrozumienie struktury projektu. W poprzednim wpisie omówiliśmy ją pobieżnie, teraz zagłębimy się w szczegóły.
my-react-app/
├── node_modules/
├── public/
│ └── vite.svg
├── src/
│ ├── assets/
│ │ └── react.svg
│ ├── App.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── .gitignore
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
Zawiera pliki statyczne, które nie przechodzą przez pipeline Vite. Są kopiowane bezpośrednio do folderu build. Idealny na pliki takie jak favicon.ico, robots.txt, manifest.json. Pliki z tego folderu dostępne są bezpośrednio przez URL: /vite.svg.
To serce naszej aplikacji. Tutaj umieszczamy wszystkie komponenty, style, utility functions, typy TypeScript, itp. Pliki z tego folderu przechodzą przez Vite (bundling, minification, transpilacja TypeScript).
W przeciwieństwie do tradycyjnych aplikacji webowych, tutaj mamy jeden plik HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Zwróćcie uwagę na dwie kluczowe rzeczy:
- <div id="root"></div> – tutaj React zamontuje całą aplikację
- <script type="module" src="/src/main.tsx"></script> – entry point naszej aplikacji
To wszystko! Cała reszta to komponenty React.
W miarę jak aplikacja rośnie, warto wprowadzić strukturę dla komponentów. Typowa organizacja wygląda tak:
src/
├── components/ # Reużywalne komponenty
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.types.ts
│ │ └── Button.module.css
│ ├── Card/
│ └── Header/
├── pages/ # Komponenty stron (dla React Router)
│ ├── Home/
│ ├── About/
│ └── Contact/
├── hooks/ # Custom hooks
├── services/ # API calls, external services
├── types/ # TypeScript types/interfaces
├── utils/ # Utility functions
├── App.tsx
└── main.tsx
Na razie nie mamy aż tylu plików, ale warto znać konwencję. W tym wpisie stworzymy kilka prostych komponentów bezpośrednio w src/.
TSX – TypeScript + JSX
Czas na kluczowy koncept: TSX (TypeScript XML). To rozszerzenie składni TypeScript pozwalające pisać coś, co wygląda jak HTML, bezpośrednio w kodzie TypeScript.
JSX to rozszerzenie składni JavaScript stworzone przez Facebook dla React. TSX to to samo, ale dla TypeScript. Pozwala pisać:
const element = <h1>Hello, World!</h1>;
Zamiast:
const element = React.createElement('h1', null, 'Hello, World!');
TSX jest syntactic sugar – pod spodem kompiluje się do wywołań React.createElement(). Ale pisanie w TSX jest o wiele bardziej intuicyjne i czytelne.
TSX wygląda jak HTML, ale to nie jest HTML. Oto kluczowe różnice:
1. className zamiast class
// ❌ To nie zadziała
<div class="container">Content</div>
// ✅ Poprawnie
<div className="container">Content</div>
Dlaczego? Bo class to zarezerwowane słowo w JavaScript/TypeScript (używane do definiowania klas).
2. CamelCase dla atrybutów
// HTML
<div onclick="handleClick()">Click me</div>
// TSX
<div onClick={handleClick}>Click me</div>
Wszystkie atrybuty w TSX używają camelCase: onClick, onChange, onSubmit, className, htmlFor (zamiast for).
3. Samozamykające się tagi
// ✅ Poprawnie
<img src="logo.png" />
<br />
<input type="text" />
// ❌ To nie zadziała
<img src="logo.png">
<br>
<input type="text">
Każdy tag bez children musi być samozamykający (z />).
4. Wyrażenia JavaScript/TypeScript w klamrach
const name = "Paweł";
const element = <h1>Witaj, {name}!</h1>;
const count = 5;
const message = <p>Masz {count * 2} nowych wiadomości</p>;
Wszystko w {} to TypeScript. Możesz umieszczać tam zmienne, wyrażenia, wywołania funkcji.
5. Fragment
Komponent może zwrócić tylko jeden element główny. Jeśli potrzebujesz zwrócić kilka elementów, użyj Fragment:
// ❌ To nie zadziała
function Component() {
return (
<h1>Tytuł</h1>
<p>Paragraf</p>
);
}
// ✅ Poprawnie z Fragment
function Component() {
return (
<>
<h1>Tytuł</h1>
<p>Paragraf</p>
</>
);
}
// ✅ Lub z React.Fragment
function Component() {
return (
<React.Fragment>
<h1>Tytuł</h1>
<p>Paragraf</p>
</React.Fragment>
);
}
<> to skrócona wersja <React.Fragment>. Używaj jej, chyba że potrzebujesz dodać key prop (o tym później).
6. Komentarze w TSX
function Component() {
return (
<div>
{/* To jest komentarz w TSX */}
<h1>Tytuł</h1>
</div>
);
}
Komentarze w TSX muszą być w {/* */}, nie w <!-- --> jak w HTML.
Komponenty funkcyjne
W React mamy dwa rodzaje komponentów: funkcyjne i klasowe. Komponenty klasowe to stary sposób (przed React 16.8), dziś standard to komponenty funkcyjne z Hooks. W tej serii skupimy się wyłącznie na komponentach funkcyjnych.
Stwórzmy pierwszy komponent. W folderze src/ utwórz plik Welcome.tsx:
function Welcome() {
return <h1>Witaj w React + TypeScript!</h1>;
}
export default Welcome;
To jest najprostszy możliwy komponent React:
- Funkcja TypeScript
- Zwraca TSX
- Export default (możemy go zaimportować w innych plikach)
Użyjmy go w App.tsx:
import Welcome from './Welcome';
function App() {
return (
<div>
<Welcome />
</div>
);
}
export default App;
Uruchom aplikację (npm run dev) i zobacz rezultat. Właśnie stworzyłeś i użyłeś swój pierwszy komponent!
- Komponenty zaczynamy wielką literą: Welcome, UserProfile, NavigationBar
- Pliki komponentów mają rozszerzenie .tsx: Welcome.tsx
- Jeden komponent = jeden plik (zazwyczaj)
To ważne! React rozpoznaje komponenty po wielkiej literze. <welcome /> nie zadziała, <Welcome /> zadziała.
Komponenty mogą zawierać inne komponenty:
function Header() {
return <header><h1>Moja Aplikacja</h1></header>;
}
function MainContent() {
return <main><p>To jest główna treść</p></main>;
}
function Footer() {
return <footer><p>© 2025 Moja Firma</p></footer>;
}
function App() {
return (
<>
<Header />
<MainContent />
<Footer />
</>
);
}
export default App;
To jest kompozycja komponentów – budujemy złożone UI z prostych, reużywalnych elementów.
Props – przekazywanie danych do komponentów
Komponenty same w sobie są mało użyteczne, jeśli zawsze wyświetlają to samo. Props (properties) to sposób na przekazywanie danych z komponentu rodzica do dziecka.
Stwórzmy komponent Greeting.tsx:
interface GreetingProps {
name: string;
}
function Greeting(props: GreetingProps) {
return <h1>Witaj, {props.name}!</h1>;
}
export default Greeting;
Użyjmy go w App.tsx:
import Greeting from './Greeting';
function App() {
return (
<div>
<Greeting name="Paweł" />
<Greeting name="Anna" />
<Greeting name="Jan" />
</div>
);
}
export default App;
Co tu się stało?
- Zdefiniowaliśmy interfejs GreetingProps – to nasze typy TypeScript dla props
- Komponent przyjmuje parametr props typu GreetingProps
- Używamy props.name w TSX
- Przekazujemy różne wartości name z App
To jest moc TypeScript! Jeśli spróbujesz:
<Greeting /> // ❌ Error: Property 'name' is missing
<Greeting name={123} /> // ❌ Error: Type 'number' is not assignable to type 'string'
<Greeting name="Paweł" age={30} /> // ❌ Error: 'age' does not exist in type 'GreetingProps'
TypeScript wyłapie błędy przed uruchomieniem aplikacji!
Zamiast props.name, props.age możemy użyć destrukturyzacji:
interface GreetingProps {
name: string;
age: number;
}
function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Witaj, {name}!</h1>
<p>Masz {age} lat</p>
</div>
);
}
export default Greeting;
To jest standard w React. Destrukturyzacja jest czystsza i krótsza.
Czasem chcemy, aby niektóre props były opcjonalne:
interface GreetingProps {
name: string;
age?: number; // Opcjonalne
}
function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Witaj, {name}!</h1>
{age && <p>Masz {age} lat</p>}
</div>
);
}
// Użycie
<Greeting name="Paweł" />
<Greeting name="Anna" age={25} />
Znak ? w TypeScript oznacza opcjonalność. {age && <p>...</p>} to renderowanie warunkowe – o tym więcej w kolejnym wpisie.
Możemy też ustawić domyślne wartości:
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
}
function Button({ label, variant = 'primary' }: ButtonProps) {
return <button className={variant}>{label}</button>;
}
// Użycie
<Button label="Kliknij" /> // variant będzie 'primary'
<Button label="Anuluj" variant="secondary" />
Specjalny prop children pozwala przekazywać zawartość między tagami komponentu:
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
}
// Użycie
<Card title="Moja Karta">
<p>To jest treść karty</p>
<button>Kliknij mnie</button>
</Card>
React.ReactNode to typ TypeScript reprezentujący wszystko, co może być renderowane w React: string, number, element, array, null, itp.
TypeScript daje nam pełną kontrolę nad typami:
interface UserCardProps {
name: string;
age: number;
email: string;
isActive: boolean;
hobbies: string[];
address: {
street: string;
city: string;
};
onEdit: () => void;
onDelete: (id: number) => void;
}
function UserCard({
name,
age,
email,
isActive,
hobbies,
address,
onEdit,
onDelete
}: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>Wiek: {age}</p>
<p>Email: {email}</p>
<p>Status: {isActive ? 'Aktywny' : 'Nieaktywny'}</p>
<p>Adres: {address.street}, {address.city}</p>
<h3>Hobby:</h3>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
<button onClick={onEdit}>Edytuj</button>
<button onClick={() => onDelete(123)}>Usuń</button>
</div>
);
}
Widzimy tutaj:
- Proste typy: string, number, boolean
- Tablice: string[]
- Obiekty: { street: string; city: string }
- Funkcje: () => void, (id: number) => void
TypeScript wymusza poprawne użycie każdego prop!
Kompozycja komponentów
Kompozycja to sposób, w jaki budujemy złożone UI z prostych, reużywalnych komponentów. To fundamentalny koncept w React.
Stwórzmy kilka małych komponentów i złóżmy je w większy:
// Avatar.tsx
interface AvatarProps {
src: string;
alt: string;
size?: number;
}
function Avatar({ src, alt, size = 50 }: AvatarProps) {
return (
<img
src={src}
alt={alt}
width={size}
height={size}
style={{ borderRadius: '50%' }}
/>
);
}
export default Avatar;
// UserInfo.tsx
interface UserInfoProps {
name: string;
email: string;
}
function UserInfo({ name, email }: UserInfoProps) {
return (
<div>
<h3>{name}</h3>
<p>{email}</p>
</div>
);
}
export default UserInfo;
// UserCard.tsx
import Avatar from './Avatar';
import UserInfo from './UserInfo';
interface UserCardProps {
avatarUrl: string;
name: string;
email: string;
}
function UserCard({ avatarUrl, name, email }: UserCardProps) {
return (
<div className="user-card">
<Avatar src={avatarUrl} alt={name} />
<UserInfo name={name} email={email} />
</div>
);
}
export default UserCard;
// App.tsx
import UserCard from './UserCard';
function App() {
return (
<div>
<UserCard
avatarUrl="https://via.placeholder.com/150"
name="Paweł Łukasiewicz"
email="pawel@example.com"
/>
</div>
);
}
export default App;
Zalety kompozycji:
- Avatar jest reużywalny – możesz użyć go w wielu miejscach
- UserInfo też jest niezależny
- UserCard łączy je w logiczną całość
- Każdy komponent robi jedną rzecz i robi ją dobrze (Single Responsibility Principle)
- Łatwe testowanie – testujesz małe jednostki
- Łatwe utrzymanie – zmiany w Avatar nie wpływają na UserInfo
W React preferujemy kompozycję nad dziedziczenie. W przeciwieństwie do programowania obiektowego, gdzie często dziedziczymy klasy, w React budujemy komponenty z innych komponentów.
Nie robimy:
// ❌ Nie rób tego w React
class BaseCard extends React.Component {}
class UserCard extends BaseCard {}
Zamiast tego:
// ✅ Rób tak
function Card({ children }) {
return <div className="card">{children}</div>;
}
function UserCard({ user }) {
return (
<Card>
<h3>{user.name}</h3>
<p>{user.email}</p>
</Card>
);
}
Praktyczny przykład: Lista produktów
Połączmy wszystko, czego się nauczyliśmy. Stworzymy prostą listę produktów.
Stwórz plik types.ts w src/:
export interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
}
Stwórz ProductCard.tsx:
import { Product } from './types';
interface ProductCardProps {
product: Product;
}
function ProductCard({ product }: ProductCardProps) {
return (
<div className="product-card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.description}</p>
<p className="price">{product.price} PLN</p>
<button>Dodaj do koszyka</button>
</div>
);
}
export default ProductCard;
Stwórz ProductList.tsx:
import { Product } from './types';
import ProductCard from './ProductCard';
interface ProductListProps {
products: Product[];
}
function ProductList({ products }: ProductListProps) {
return (
<div className="product-list">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export default ProductList;
Zwróć uwagę na key={product.id}. Gdy renderujemy listę w React, każdy element musi mieć unikalny key. To pomaga React efektywnie aktualizować DOM. O key więcej powiemy w kolejnym wpisie.
import ProductList from './ProductList';
import { Product } from './types';
function App() {
const products: Product[] = [
{
id: 1,
name: 'Laptop Dell XPS 15',
price: 5999,
description: 'Potężny laptop dla profesjonalistów',
imageUrl: 'https://via.placeholder.com/200'
},
{
id: 2,
name: 'iPhone 15 Pro',
price: 4999,
description: 'Najnowszy smartfon od Apple',
imageUrl: 'https://via.placeholder.com/200'
},
{
id: 3,
name: 'Sony WH-1000XM5',
price: 1499,
description: 'Słuchawki z redukcją szumów',
imageUrl: 'https://via.placeholder.com/200'
}
];
return (
<div className="app">
<h1>Sklep Elektroniczny</h1>
<ProductList products={products} />
</div>
);
}
export default App;
Dodaj do App.css:
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.product-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.product-card img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
}
.product-card h3 {
margin: 10px 0;
}
.product-card .price {
font-size: 20px;
font-weight: bold;
color: #2ecc71;
margin: 10px 0;
}
.product-card button {
background-color: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.product-card button:hover {
background-color: #2980b9;
}
Uruchom aplikację – masz działającą listę produktów z pełnym TypeScript support! 🎉
Best Practices dla Props i komponentów
Na koniec kilka dobrych praktyk, które warto znać od początku:
// ❌ Źle
interface Props {
d: string;
fn: () => void;
}
// ✅ Dobrze
interface ButtonProps {
label: string;
onClick: () => void;
}
// ❌ Źle - komponent robi za dużo
function UserDashboard() {
return (
<div>
{/* user profile */}
{/* notifications */}
{/* settings */}
{/* statistics */}
</div>
);
}
// ✅ Dobrze - podział na mniejsze komponenty
function UserDashboard() {
return (
<div>
<UserProfile />
<Notifications />
<Settings />
<Statistics />
</div>
);
}
// ❌ Źle - brak typów
function User(props) {
return <div>{props.name}</div>;
}
// ✅ Dobrze - pełne typy
interface UserProps {
name: string;
age: number;
}
function User({ name, age }: UserProps) {
return <div>{name}, {age}</div>;
}
Jeśli komponent przyjmuje więcej niż 5-7 props, zastanów się czy nie powinien być podzielony na mniejsze komponenty.
// ❌ Może być lepiej
interface UserCardProps {
firstName: string;
lastName: string;
email: string;
phone: string;
address: string;
city: string;
country: string;
// ... 10 więcej props
}
// ✅ Lepiej
interface User {
firstName: string;
lastName: string;
email: string;
phone: string;
address: Address;
}
interface Address {
street: string;
city: string;
country: string;
}
interface UserCardProps {
user: User;
}
interface ChartProps {
/**
* Dane do wykresu w formacie { x: number, y: number }[]
*/
data: Array<{ x: number; y: number }>;
/**
* Kolor linii wykresu (hex, rgb, lub nazwa)
* @default '#3498db'
*/
lineColor?: string;
/**
* Callback wywoływany gdy użytkownik kliknie punkt na wykresie
*/
onPointClick?: (point: { x: number; y: number }) => void;
}
JSDoc komentarze są widoczne w IntelliSense – bardzo przydatne dla innych developerów (i dla Ciebie za 6 miesięcy!).
Podsumowanie
W tym wpisie nauczyliśmy się fundamentów React + TypeScript:
- ✅ Struktura projektu – jak organizować pliki i foldery
- ✅ TSX – składnia, różnice od HTML, dobre praktyki
- ✅ Komponenty funkcyjne – jak je tworzyć, konwencje nazewnictwa
- ✅ Props – przekazywanie danych, typowanie z TypeScript, opcjonalne props, domyślne wartości
- ✅ Kompozycja – budowanie złożonych UI z prostych komponentów
- ✅ Praktyczny przykład – lista produktów z pełnym TypeScript support
To solidny fundament! W kolejnym wpisie zagłębimy się w stan komponentów (useState), nauczymy się obsługiwać zdarzenia, poznamy renderowanie warunkowe i listy. Zaczniemy budować interaktywne komponenty, które reagują na działania użytkownika.
Do zobaczenia w części trzeciej! 🚀