English flagEnglish

6. gyakorlat - Statikus prototípus átültetése React komponensekbe

2025-03-21 4 perc olvasási idő GitHub

Bevezetés

Az előző órán megismerkedtünk a React alapjaival: komponensek, JSX, props, TypeScript típusozás, és a Vite build eszköz. Egy egyszerű Hello komponenst készítettünk.

A mai órán valódi projektet csinálunk belőle. Kiindulópontunk egy kész statikus HTML prototípus — ez egy lejátszólista-kezelő alkalmazás (playlists.html, tracks.html). A feladatunk ezt React komponensekre bontani, TypeScript típusokkal megtámogatva.

A mai óra után képes leszel:

  • Statikus HTML struktúrát React komponensekbe szervezni
  • JSX és HTML közötti különbségeket felismerni és kijavítani (className, önzáró tagek)
  • Képeket és más statikus fájlokat Vite-tal importálni
  • Adatokat .map()-pel kirenderni, és a key prop fontosságát érteni
  • TypeScript interface-t definiálni domain adathoz
  • Külső CSS keretrendszert (Fomantic UI) bekapcsolni a projektbe

A feladattár és a projekt

A 6. gyakorlattól a kliensoldali webprogramozás feladattárából dolgozunk:

https://github.com/horvathgyozo/kliensoldali-webprogramozas-feladattar
⚠️

A feladattárból a typescript branch-et töltsd le, nem a main-t! A branch váltásához a GitHub oldalán a branch-választó legördülőn kattints a typescript névre.

A mai feladat: myplaylist-sitebuild-components

Projekt struktúra

myplaylist-sitebuild-components/
├── sitebuild/               # Kész statikus prototípus — innen dolgozunk
│   ├── playlists.html
│   ├── tracks.html
│   └── assets/
│       ├── logo.png
│       └── bonjovi.jpg
├── src/
│   ├── main.tsx
│   ├── App.tsx              # Gyökérkomponens
│   ├── components/
│   │   ├── Navbar.tsx
│   │   ├── Playlists.tsx
│   │   ├── SongList.tsx
│   │   └── SongCard.tsx
│   └── domain/
│       ├── playlist.ts      # IPlaylist interface + adatok
│       └── track.ts         # ITrack interface + adatok
└── index.html

1. feladat: Fomantic UI betöltése

A statikus prototípus a Fomantic UI CSS keretrendszert használja (a Semantic UI közösségi fork-ja). Az osztályok — pl. ui menu, ui list, ui segment — ennek a keretrendszernek a komponensei.

Legegyszerűbb betölteni az index.html-ben CDN-ről:

<!-- index.html -->
<head>
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.4/dist/semantic.min.css"
  />
</head>
<body class="ui container">
  <div id="root"></div>

  <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.4/dist/semantic.min.js"></script>
  <script type="module" src="/src/main.tsx"></script>
</body>

A <body class="ui container"> gondoskodik az oldal középre igazításáról — ezt a Fomantic UI oldja meg.


2. feladat: HTML → JSX — a különbségek

Amikor egy HTML fájlból JSX-be másoljuk a tartalmat, néhány dolgot kötelezően javítani kell. A böngésző és a React között az a különbség, hogy a JSX valójában JavaScript — az attribútumnevek ezért a DOM property neveket követik, nem a HTML attribútumokat.

classclassName

// HTML:
<div class="ui menu">

// JSX:
<div className="ui menu">

A class JavaScriptben foglalt szó (osztálydefinícióhoz). Ezért a JSX a DOM-beli className property nevet használja.

Önzáró tagek

HTML-ben néhány elem nem igényel záró taget (<img>, <input>, <br>). JSX-ben ezeket kötelező önzáróan írni:

// HTML:
<img src="logo.png">
<input type="text">

// JSX:
<img src="logo.png" />
<input type="text" />

Ha ezt kihagyod, a Vite fordítóhiba jelent (a TypeScript is hibát jelez, mert a JSX XML-alapú szintaxist követ).

forhtmlFor

// HTML:
<label for="email">Email</label>

// JSX:
<label htmlFor="email">Email</label>

A for szintén foglalt szó JavaScriptben (ciklus). A DOM-ban az ekvivalens property htmlFor.

ℹ️

Minden attribútum, ami HTML-ben kötőjeles, JSX-ben camelCase lesz: tabindextabIndex, crossorigincrossOrigin. Kivétel: a data-* és aria-* attribútumok változatlan formájukban maradnak.


3. feladat: Képimport Vite-tal

A statikus prototípusban a logó és a borítóképek relatív URL-lel hivatkoznak a fájlokra (assets/logo.png). Vite-ban másképp dolgozunk: a képeket JavaScript modulként importáljuk, és az importált értéket adjuk az src attribútumnak.

// Navbar.tsx
import logo from '../../sitebuild/assets/logo.png';

const Navbar = () => {
  return (
    <nav className="ui secondary menu">
      <img src={logo} />
      {/* ... */}
    </nav>
  );
};

A logo változó értéke futásidőben egy URL string lesz — Vite a build folyamat során kezeli a fájl elhelyezését és a helyes URL generálását.

💡

Ez a minta minden statikus fájlra (kép, font, SVG, JSON) érvényes. Az import révén Vite tudja, hogy melyik fájlok vannak ténylegesen használatban, és csak azokat csomagolja be a produkciós buildbe.


4. feladat: Az App komponens felépítése

Elsőként az App.tsx-be másoljuk be az összes HTML tartalmat (a playlists.html <body>-jából), és fokozatosan bontjuk ki komponensekbe.

A végső App.tsx az összerakott alkalmazást mutatja:

// App.tsx
import Navbar from "./components/Navbar";
import Playlists from "./components/Playlists";
import SongList from "./components/SongList";
import SongCard from "./components/SongCard";

function App() {
  return (
    <div>
      <Navbar />
      <div className="ui container">
        <h1>My Playlists</h1>
        <div className="ui stackable two column grid">
          <Playlists />
          <SongList />
        </div>
        <div className="ui divider"></div>
        <SongCard />
      </div>
    </div>
  );
}

export default App;

Az App komponens nem tartalmaz saját megjelenítési logikát — csak összeszervezi a gyermekkomponenseket. Ez a komponensalapú gondolkodás lényege.


5. feladat: Navbar komponens

A navigáció egy önálló komponensbe kerül. A logót importálni kell Vite-tal:

// src/components/Navbar.tsx
import logo from '../../sitebuild/assets/logo.png';

const Navbar = () => {
  return (
    <nav className="ui secondary menu">
      <img src={logo} />
      <a className="item" href="index.html">
        <i className="home icon"></i> Home
      </a>
      <a className="active item" href="playlists.html">
        <i className="headphones icon"></i> My Playlists
      </a>
      <a className="item" href="tracks.html">
        <i className="music icon"></i> Tracks
      </a>
      <a className="item" href="search.html">
        <i className="search icon"></i> Search
      </a>
      <div className="ui right dropdown item">
        John Doe
        <i className="dropdown icon"></i>
        <div className="menu">
          <div className="item"><i className="user icon"></i> Profile</div>
          <div className="item"><i className="settings icon"></i> Settings</div>
          <div className="item"><i className="sign out icon"></i> Log out</div>
        </div>
      </div>
    </nav>
  );
};

export default Navbar;

6. feladat: Domain adatok TypeScript-tel

Mielőtt a Playlists komponenst megírjuk, definiáljuk az adatstruktúrát. A domain/ mappa tartalmazza a típusokat és a mintaadatokat.

ITrack interface

// src/domain/track.ts
import bonjoviCover from "../../sitebuild/assets/bonjovi.jpg";

export interface ITrack {
  id: number;
  artist: string;
  title: string;
  length: string;
  thumbnailURL?: string;
  chordsURL?: string;
  lyricsURL?: string;
  spotifyURL?: string;
}

export const exampleTracks: ITrack[] = [
  {
    id: 1,
    artist: "Bon Jovi",
    title: "It's my life",
    length: "3:44",
    thumbnailURL: bonjoviCover,
    spotifyURL: "https://open.spotify.com/track/0v1XpBHnsbkCn7iJ9Ucr1l",
    chordsURL: "https://tabs.ultimate-guitar.com/tab/bon-jovi/its-my-life-chords-951538",
    lyricsURL: "https://www.azlyrics.com/lyrics/bonjovi/itsmylife.html",
  },
  {
    id: 2, artist: "Bon Jovi", title: "Livin' on a prayer", length: "4:11",
  },
  // ...
];

Az opcionális mezők (?) azt jelzik, hogy nem minden zeneszámhoz áll rendelkezésre minden adat — pl. nem minden dalhoz van Spotify link. Ez az előző órán tanult ? opcionális szintaxis valódi alkalmazása.

IPlaylist interface

// src/domain/playlist.ts
import type { ITrack } from "./track";
import { exampleTracks } from "./track";

export interface IPlaylist {
  id: number;
  title: string;
  tracks: ITrack[];
}

export const examplePlaylists: IPlaylist[] = [
  { id: 1, title: "Heavy Metal",        tracks: exampleTracks.slice(0, 4) },
  { id: 2, title: "Classics",           tracks: exampleTracks.slice(0, 3) },
  { id: 3, title: "Movie Soundtracks",  tracks: exampleTracks.slice(2, 3) },
  { id: 4, title: "Hip-Hop",            tracks: [] },
];

import type

import type { ITrack } from "./track";

A import type csak a TypeScript fordítónak szóló információ — futásidőben semmilyen kód nem generálódik belőle. Mivel az ITrack csak egy típusdefiníció (nem értéket exportál), célszerű import type-ként behúzni. Ha csak simán import-ot írsz, az is működik, de import type szemantikailag pontosabb.


7. feladat: Playlists komponens — lista renderelése

Ez a komponens az előző feladatban definiált examplePlaylists adatot használja, és abból generálja a listát.

// src/components/Playlists.tsx
import { examplePlaylists, type IPlaylist } from "../domain/playlist";

const Playlists = () => {
  return (
    <div className="ui six wide column">
      <h3>Playlists</h3>
      <div className="ui very relaxed selection list">

        {examplePlaylists
          .sort((a, b) => a.title.localeCompare(b.title))
          .map((playlist: IPlaylist) => (
            <div className="item" key={playlist.id}>
              <i className="large compact disc middle aligned icon"></i>
              <div className="content">
                <a className="header">{playlist.title}</a>
                <div className="description">{playlist.tracks.length} songs</div>
              </div>
            </div>
          ))
        }

        <div className="item" id="newPlaylist">
          <i className="large green plus middle aligned icon"></i>
          <div className="content">
            <a className="header">New</a>
            <div className="description">Create a new playlist</div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Playlists;

A key prop

React listáknál minden elemet egyedi key prop-pal kell ellátni:

.map((playlist) => (
  <div key={playlist.id}>...</div>
))

A key nem jelenik meg a DOM-ban — React belső azonosítóként használja, hogy hatékonyan tudja frissíteni a listát, ha az adatok megváltoznak. Nélküle React figyelmeztetést dob, és a frissítések lassabbak vagy hibásak lehetnek.

⚠️

Soha ne használj tömbindexet (i-t) key-ként, ha a lista sorrendje változhat! Az id mező az ideális kulcs, mert egyedi és stabil.

Ábécé szerint rendezés — localeCompare

.sort((a, b) => a.title.localeCompare(b.title))

A localeCompare az egyszerű < / > összehasonlítás helyett a böngésző locale-érzékeny összehasonlítóját használja. Ez fontos, ha az adatokban ékezetes karakterek is lehetnek (pl. „Ábécé” < „Zene” helyesen rendeződjön).


8. feladat: SongList és SongCard komponensek

A SongList és SongCard egyelőre statikusan tartalmazzák a tartalmat — az adataikat a statikus prototípusból másoltuk be:

// src/components/SongList.tsx
const SongList = () => {
  return (
    <div className="ui ten wide column">
      <h3>Classics</h3>
      <div className="ui very relaxed selection list">
        <div className="item">
          <i className="large music middle aligned icon"></i>
          <div className="content">
            <a className="header">Highway to hell</a>
            <div className="description">AC/DC</div>
          </div>
        </div>
        {/* ... többi dal ... */}
        <div className="active item">
          <i className="large music middle aligned icon"></i>
          <div className="content">
            <a className="header">It's my life</a>
            <div className="description">Bon Jovi</div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default SongList;
// src/components/SongCard.tsx
import bonjoviCover from "../../sitebuild/assets/bonjovi.jpg";

const SongCard = () => {
  return (
    <div className="ui segment">
      <div className="ui items">
        <div className="item">
          <div className="image">
            <img src={bonjoviCover} />
          </div>
          <div className="content">
            <a className="header">It's my life</a>
            <div className="meta">
              <span>Bon Jovi</span>
              <span>4:35</span>
            </div>
            <div className="extra">
              <a
                href="https://open.spotify.com/track/0v1XpBHnsbkCn7iJ9Ucr1l"
                className="ui button tiny green button"
                target="_blank"
              >
                <i className="spotify icon"></i> Listen on Spotify
              </a>
              <a
                href="https://tabs.ultimate-guitar.com/tab/bon-jovi/its-my-life-chords-951538"
                className="ui button tiny teal button"
                target="_blank"
              >
                <i className="microphone icon"></i> Show lyrics
              </a>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default SongCard;

JSX vs HTML — összefoglaló

HTMLJSXMiért?
class="..."className="..."class foglalt szó JavaScriptben
for="..."htmlFor="..."for foglalt szó JavaScriptben
<img src="..."><img src="..." />JSX megköveteli az önzáró tageket
<input><input />Ugyanaz
tabindextabIndexcamelCase DOM property nevek
<!-- megjegyzés -->{/* megjegyzés */}JSX komment szintaxis
style="color: red"style={{ color: 'red' }}Objektum, camelCase tulajdonságnevekkel

Összefoglalás

TémaKulcsfogalmak
JSX különbségekclassName, htmlFor, önzáró tagek, {/* */} komment
Képimportimport logo from './assets/logo.png', Vite kezeli a path-t
import typeCsak típusokat importál, futásidejű kódot nem generál
Lista renderelés.map() JSX-ben, key prop kötelező
key propEgyedi, stabil azonosító (pl. id); ne legyen tömb index
localeCompareLocale-érzékeny string összehasonlítás rendezéshez
Komponens kiemelésLogikus egységenként: Navbar, Playlists, SongList, SongCard
Domain adatokinterface + adattömb importálva a komponensbe
Fomantic UISemantic UI fork, class-alapú CSS rendszer