Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

W 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

Folder public/

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.

Folder src/

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).

Plik index.html

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:

  1. <div id="root"></div> – tutaj React zamontuje całą aplikację
  2. <script type="module" src="/src/main.tsx"></script> – entry point naszej aplikacji

To wszystko! Cała reszta to komponenty React.

Organizacja komponentów (best practices)

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.

Czym jest JSX/TSX?

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 vs HTML – różnice

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.

Pierwszy prosty komponent

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!

Konwencje nazewnictwa

  • 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 zagnieżdżone

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.

Prosty przykład Props

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?

  1. Zdefiniowaliśmy interfejs GreetingProps – to nasze typy TypeScript dla props
  2. Komponent przyjmuje parametr props typu GreetingProps
  3. Używamy props.name w TSX
  4. 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!

Destrukturyzacja Props

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.

Opcjonalne Props

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.

Domyślne wartości Props

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" />

Props.children

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.

Typy dla różnych Props

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.

Przykład: Karta użytkownika

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

Kompozycja vs dziedziczenie

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.

Krok 1: Typ produktu

Stwórz plik types.ts w src/:

export interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
}

Krok 2: Komponent ProductCard

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;

Krok 3: Komponent ProductList

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.

Krok 4: Użycie w App

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;

Krok 5: Style (opcjonalnie)

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:

1. Nazywaj Props opisowo

// ❌ Źle
interface Props {
  d: string;
  fn: () => void;
}

// ✅ Dobrze
interface ButtonProps {
  label: string;
  onClick: () => void;
}

2. Jeden komponent = jedna odpowiedzialność

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

3. Używaj TypeScript konsekwentnie

// ❌ Ź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>;
}

4. Nie przekazuj zbyt wielu props

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

5. Dokumentuj złożone Props

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! 🚀

Trzecia część artykułu: React - wprowadzenie: część III (State, useState Hook, zdarzenia, formularze, renderowanie warunkowe)