6. gyakorlat - Statikus prototípus átültetése React komponensekbe
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 akeyprop 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.
class → className
// 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).
for → htmlFor
// 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: tabindex → tabIndex, crossorigin → crossOrigin. 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ó
| HTML | JSX | Mié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 |
tabindex | tabIndex | camelCase DOM property nevek |
<!-- megjegyzés --> | {/* megjegyzés */} | JSX komment szintaxis |
style="color: red" | style={{ color: 'red' }} | Objektum, camelCase tulajdonságnevekkel |
Összefoglalás
| Téma | Kulcsfogalmak |
|---|---|
| JSX különbségek | className, htmlFor, önzáró tagek, {/* */} komment |
| Képimport | import logo from './assets/logo.png', Vite kezeli a path-t |
import type | Csak típusokat importál, futásidejű kódot nem generál |
| Lista renderelés | .map() JSX-ben, key prop kötelező |
key prop | Egyedi, stabil azonosító (pl. id); ne legyen tömb index |
localeCompare | Locale-érzékeny string összehasonlítás rendezéshez |
| Komponens kiemelés | Logikus egységenként: Navbar, Playlists, SongList, SongCard |
| Domain adatok | interface + adattömb importálva a komponensbe |
| Fomantic UI | Semantic UI fork, class-alapú CSS rendszer |