React-Komponenten sind keine HTML-Templates. Sie sind JavaScript-Funktionen, die UI beschreiben. Aber diese Beschreibung sieht aus wie HTML – Syntax mit Tags, Attributen, Verschachtelung. Das ist kein Zufall, sondern Design. JSX (JavaScript XML) und seine TypeScript-Variante TSX erlauben es, UI-Strukturen direkt in Code zu schreiben, als wären sie Markup.
Aber das ist kein Markup. Es ist Code. Funktionsaufrufe, getarnt als
Tags. Jedes <div> ist ein
React.createElement('div', ...). Jedes
<Button> ist ein Funktionsaufruf. Die HTML-ähnliche
Syntax ist syntaktischer Zucker – eine lesbare Schreibweise für
verschachtelte Funktionsaufrufe.
Dieses Kapitel erklärt TSX als das, was es ist: Eine eigene Syntax für React-Komponenten. Keine Template-Sprache, kein Preprocessor-Trick, sondern TypeScript-Code mit spezieller Syntax. Wir bauen Komponenten von Grund auf, verstehen Props, Typisierung und Komposition. Das volle Programm.
TSX sieht aus wie HTML:
const greeting = <h1>Hello World</h1>;
Aber es ist TypeScript. Der TypeScript-Compiler transformiert diese Zeile zu:
const greeting = React.createElement('h1', null, 'Hello World');React.createElement ist eine Funktion, die ein
JavaScript-Objekt zurückgibt – ein Element-Deskriptor. Dieses Objekt
beschreibt, was gerendert werden soll. React nimmt diese Deskriptoren
und erstellt daraus echtes DOM.
// TSX-Syntax
const element = <h1 className="title">Hello</h1>;
// Wird zu
const element = React.createElement(
'h1',
{ className: 'title' },
'Hello'
);
// Ergibt ein Objekt
{
type: 'h1',
props: {
className: 'title',
children: 'Hello'
}
}
Diese Transformation passiert zur Build-Zeit. Im Browser läuft kein TSX-Code, sondern nur normales JavaScript.
JSX ist die JavaScript-Variante, TSX die TypeScript-Variante. Der Unterschied: Typsicherheit. TSX prüft zur Build-Zeit, ob Props korrekt sind, ob Komponenten existieren, ob Typen stimmen. JSX nicht.
Aus diesem Grund fokussieren wir auf TSX. TypeScript ist heute Standard in professionellen React-Projekten. JSX ist legacy.
TSX ähnelt HTML, aber mit wichtigen Unterschieden.
1. Selbstschließende Tags müssen geschlossen werden
// ❌ HTML: funktioniert
<img src="logo.png">
<br>
// ✓ TSX: muss geschlossen werden
<img src="logo.png" />
<br />
2. Attributnamen sind camelCase
// ❌ HTML-Attribute
<div class="container" tabindex="0"></div>
// ✓ TSX-Props
<div className="container" tabIndex={0}></div>
class ist ein JavaScript-Keyword, deshalb
className. for ist ein Keyword, deshalb
htmlFor. Alle Event-Handler sind camelCase:
onClick, onChange, onSubmit.
3. JavaScript-Expressions in geschweiften Klammern
const name = 'Alice';
const age = 30;
// JavaScript-Werte einbetten
<div>
<h1>Hello {name}</h1>
<p>Age: {age}</p>
<p>Next year: {age + 1}</p>
<p>Is adult: {age >= 18 ? 'Yes' : 'No'}</p>
</div>
Alles zwischen {} ist JavaScript. Variablen,
Berechnungen, Funktionsaufrufe, ternäre Operatoren.
4. Nur ein Root-Element
// ❌ Fehler: Mehrere Root-Elemente
function Component() {
return (
<h1>Title</h1>
<p>Text</p>
);
}
// ✓ Ein Root-Element
function Component() {
return (
<div>
<h1>Title</h1>
<p>Text</p>
</div>
);
}
// ✓ Oder Fragment (kein DOM-Element)
function Component() {
return (
<>
<h1>Title</h1>
<p>Text</p>
</>
);
}
Fragments (<>) sind unsichtbare Wrapper. Sie
gruppieren Elemente ohne zusätzlichen DOM-Knoten.
Das Mächtige an TSX: Sie können eigene Tags erstellen. Komponenten sind Funktionen, die TSX zurückgeben. Diese Funktionen werden zu Tags.
// Komponente definieren
function Button() {
return <button className="btn">Click me</button>;
}
// Komponente verwenden
function App() {
return (
<div>
<h1>My App</h1>
<Button />
<Button />
</div>
);
}
<Button /> ist ein selbst definiertes Tag. Es
sieht aus wie HTML, ist aber ein Funktionsaufruf. React erkennt
Komponenten daran, dass sie mit Großbuchstaben beginnen. Kleinbuchstaben
sind native HTML-Tags (<div>,
<button>), Großbuchstaben sind Komponenten
(<Button>, <UserProfile>).
// HTML-Tag
<button>Click</button> // → React.createElement('button', ...)
// Komponente
<Button /> // → React.createElement(Button, ...)
Komponenten sind Bausteine. Sie kapseln UI-Logik, Struktur und Verhalten. Einmal definiert, mehrfach verwendbar.
function Card() {
return (
<div className="card">
<h2>Card Title</h2>
<p>Card content goes here</p>
</div>
);
}
function Dashboard() {
return (
<div className="dashboard">
<Card />
<Card />
<Card />
</div>
);
}
Drei <Card />-Instanzen, drei separate Kopien im
DOM. Jede eigenständig.
Komponenten ohne Daten sind nutzlos. Props (Properties) machen Komponenten konfigurierbar. Sie sind Parameter für Komponenten-Funktionen.
// Props als Funktion-Parameter
function Greeting(props: { name: string }) {
return <h1>Hello {props.name}</h1>;
}
// Verwendung
<Greeting name="Alice" />
<Greeting name="Bob" />
Props werden als Attribute übergeben, genau wie HTML-Attribute. Aber sie sind typisiert.
TypeScript-Interface für Props:
interface GreetingProps {
name: string;
age: number;
}
function Greeting(props: GreetingProps) {
return (
<div>
<h1>Hello {props.name}</h1>
<p>Age: {props.age}</p>
</div>
);
}
// Verwendung
<Greeting name="Alice" age={30} />
TypeScript prüft, ob alle Props vorhanden sind und die richtigen Typen haben.
// ❌ Fehler: 'age' fehlt
<Greeting name="Alice" />
// ❌ Fehler: 'age' ist string, sollte number sein
<Greeting name="Alice" age="30" />
// ✓ Korrekt
<Greeting name="Alice" age={30} />
Destructuring für cleanen Code:
function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Hello {name}</h1>
<p>Age: {age}</p>
</div>
);
}
Statt props.name und props.age direkt
name und age. Weniger Tippen, bessere
Lesbarkeit.
Optionale Props:
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary'; // Optional
}
function Button({ text, variant = 'primary' }: ButtonProps) {
return <button className={`btn btn-${variant}`}>{text}</button>;
}
// Beide gültig
<Button text="Click me" />
<Button text="Submit" variant="secondary" />
Das ? markiert Props als optional. Default-Werte in der
Funktion-Signatur setzen Fallback-Werte.
Children: Spezielle Prop für verschachtelte Inhalte:
interface CardProps {
title: string;
children: React.ReactNode; // Alles, was gerendert werden kann
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
}
// Verwendung
<Card title="My Card">
<p>This is the card content.</p>
<button>Action</button>
</Card>
children ist eine spezielle Prop. Alles zwischen
öffnendem und schließendem Tag wird als children
übergeben.
Props sind nicht auf Strings und Numbers beschränkt. Objekte, Arrays, Funktionen – alles ist möglich.
Objekte als Props:
interface User {
id: number;
name: string;
email: string;
}
interface UserCardProps {
user: User;
}
function UserCard({ user }: UserCardProps) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
// Verwendung
const alice = { id: 1, name: 'Alice', email: 'alice@example.com' };
<UserCard user={alice} />
Arrays als Props:
interface ListProps {
items: string[];
}
function List({ items }: ListProps) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
// Verwendung
<List items={['Apple', 'Banana', 'Cherry']} />
Funktionen als Props (Event Handler):
interface ButtonProps {
text: string;
onClick: () => void;
}
function Button({ text, onClick }: ButtonProps) {
return <button onClick={onClick}>{text}</button>;
}
// Verwendung
function App() {
const handleClick = () => {
alert('Button clicked!');
};
return <Button text="Click me" onClick={handleClick} />;
}
Funktionen als Props ermöglichen Kommunikation von Child zu Parent. Der Parent übergibt eine Callback-Funktion, die Child aufruft.
UI ist selten statisch. Oft zeigen wir Inhalte basierend auf Bedingungen.
Ternärer Operator:
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return (
<div>
{isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please log in</h1>}
</div>
);
}
Logical AND (&&):
function Notification({ message }: { message?: string }) {
return (
<div>
{message && <div className="notification">{message}</div>}
</div>
);
}
// Zeigt nichts, wenn message undefined oder leer
<Notification />
<Notification message="" />
// Zeigt Notification
<Notification message="New message!" />
Vorsicht mit Zahlen:
// ❌ Falsch: Zeigt "0" wenn count === 0
{count && <div>Count: {count}</div>}
// ✓ Richtig: Explizite Bedingung
{count > 0 && <div>Count: {count}</div>}
0 ist falsy in JavaScript, aber React rendert es.
Explizite Bedingungen vermeiden dieses Problem.
Frühe Returns für komplexe Bedingungen:
function UserProfile({ user }: { user?: User }) {
if (!user) {
return <div>Loading...</div>;
}
if (user.role === 'admin') {
return <AdminPanel user={user} />;
}
return <UserPanel user={user} />;
}
Mehrere return-Statements machen komplexe Bedingungen
lesbarer.
Listen sind allgegenwärtig. Produkte, Benutzer, Nachrichten.
map() ist das Standard-Tool.
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: Todo[];
}
function TodoList({ todos }: TodoListProps) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}
map() transformiert das Array von Todos zu einem Array
von <li>-Elementen. React rendert dieses Array.
Keys sind essentiell:
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
Jedes Element braucht eine key-Prop. Keys helfen React,
Elemente zu identifizieren. Wenn sich die Liste ändert (Elemente
hinzugefügt, entfernt, neu sortiert), kann React anhand der Keys
feststellen, was sich geändert hat.
Niemals Array-Index als Key verwenden (außer bei statischen Listen):
// ❌ Problematisch bei dynamischen Listen
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
// ✓ Verwenden Sie eindeutige IDs
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
Warum? Wenn die Liste sich ändert (Element wird gelöscht), verschieben sich die Indizes. React denkt, Element 0 ist immer noch Element 0, obwohl es jetzt ein anderes Todo ist. Das führt zu Bugs mit Component-State.
Filtern und Mappen kombinieren:
function TodoList({ todos }: TodoListProps) {
return (
<ul>
{todos
.filter(todo => !todo.completed)
.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Erst filtern (nur unvollständige Todos), dann mappen (in
<li>-Elemente).
UI ohne Interaktion ist nutzlos. Events bringen Leben in Komponenten.
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
Event-Handler sind Props. onClick,
onChange, onSubmit – alle folgen dem
on[EventName]-Pattern.
Event-Objekt:
function Input() {
const [value, setValue] = useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
return <input value={value} onChange={handleChange} />;
}
TypeScript typisiert Event-Objekte.
React.ChangeEvent<HTMLInputElement> ist der Typ für
Change-Events auf Input-Elementen.
Inline Event-Handler:
<button onClick={() => setCount(count + 1)}>Increment</button>
Für einfache Logik OK. Für komplexe Logik: Separate Funktionen.
Event Propagation verhindern:
const handleClick = (event: React.MouseEvent) => {
event.stopPropagation(); // Verhindert Bubbling
// ...
};
Form Handling:
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault(); // Verhindert Seiten-Reload
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
);
}
event.preventDefault() ist kritisch bei Forms. Ohne es
lädt die Seite neu.
Der wahre Wert von Komponenten liegt in Komposition. Komponenten in Komponenten in Komponenten.
// Atomare Komponente
function Button({ text, onClick }: ButtonProps) {
return <button onClick={onClick}>{text}</button>;
}
// Komponierte Komponente
function LoginForm() {
return (
<form>
<input type="email" />
<input type="password" />
<Button text="Login" onClick={() => {}} />
</form>
);
}
// Höhere Ebene
function App() {
return (
<div>
<header>
<h1>My App</h1>
</header>
<main>
<LoginForm />
</main>
</div>
);
}
Jede Ebene abstrahiert Komplexität. App weiß nichts über
Buttons. LoginForm weiß nichts über die Header-Struktur.
Separation of Concerns.
Layout-Komponenten:
interface PageProps {
title: string;
children: React.ReactNode;
}
function Page({ title, children }: PageProps) {
return (
<div className="page">
<header>
<h1>{title}</h1>
</header>
<main>{children}</main>
<footer>© 2025</footer>
</div>
);
}
// Verwendung
function HomePage() {
return (
<Page title="Home">
<p>Welcome to the home page!</p>
</Page>
);
}
function AboutPage() {
return (
<Page title="About">
<p>About us...</p>
</Page>
);
}
Page ist eine Layout-Komponente. Sie definiert Struktur,
Content kommt via children.
Bringen wir alles zusammen.
// Types
interface Todo {
id: number;
text: string;
completed: boolean;
}
// TodoItem Komponente
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<li className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
// TodoList Komponente
interface TodoListProps {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
if (todos.length === 0) {
return <p>No todos yet. Add one!</p>;
}
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
// AddTodoForm Komponente
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
function AddTodoForm({ onAdd }: AddTodoFormProps) {
const [text, setText] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
onAdd(text);
setText('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
);
}
// App Komponente
function App() {
const [todos, setTodos] = useState<Todo[]>([]);
const addTodo = (text: string) => {
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="app">
<h1>Todo App</h1>
<AddTodoForm onAdd={addTodo} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
Diese App demonstriert: - Komponentenhierarchie (App → TodoList → TodoItem) - Props mit TypeScript (typsichere Interfaces) - Event-Handling (onToggle, onDelete, onAdd) - Conditional Rendering (leere Liste) - Listen mit map() und keys - State-Management (useState) - Forms (controlled inputs)
TSX ermöglicht es, all das lesbar, wartbar und typsicher zu schreiben.
JSX funktioniert. Aber TSX ist besser. Drei Gründe:
1. Typsicherheit zur Build-Zeit
// TSX
<Greeting name="Alice" age={30} /> // ✓
<Greeting name="Alice" /> // ❌ Fehler: 'age' fehlt
// JSX
<Greeting name="Alice" age={30} /> // ✓
<Greeting name="Alice" /> // ✓ Läuft, crasht zur Runtime
2. Bessere IDE-Unterstützung
Autocomplete für Props, Inline-Dokumentation, Refactoring-Tools.
TypeScript weiß, was Greeting erwartet.
3. Selbst-dokumentierender Code
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
onClick: () => void;
}
Das Interface ist Dokumentation. Jeder, der Button
verwendet, weiß sofort, welche Props existieren und welche Typen sie
haben.
1. Vergessen, Komponenten-Namen groß zu schreiben
// ❌ React denkt, es ist ein HTML-Tag
function button() { return <button>Click</button>; }
<button />
// ✓ Großbuchstabe → React erkennt Komponente
function Button() { return <button>Click</button>; }
<Button />
2. Strings in Props verwenden, wo JSX-Expressions erwartet werden
// ❌ Falsch: String statt Number
<Greeting age="30" />
// ✓ Richtig: JavaScript-Expression
<Greeting age={30} />
3. Event-Handler direkt aufrufen statt Funktion zu übergeben
// ❌ Ruft Funktion sofort auf
<button onClick={handleClick()}>Click</button>
// ✓ Übergibt Funktion
<button onClick={handleClick}>Click</button>
// ✓ Oder Arrow-Function für Parameter
<button onClick={() => handleClick(id)}>Click</button>
4. Keys vergessen bei Listen
// ❌ Fehler: Keine keys
{todos.map(todo => <li>{todo.text}</li>)}
// ✓ Keys hinzufügen
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
TSX ist nicht HTML. Es ist eine eigene Syntax für React-Komponenten. Eine Syntax, die UI-Strukturen ausdrückt, aber in TypeScript lebt. Komponenten sind Funktionen. Props sind Parameter. Tags sind Funktionsaufrufe. Alles typsicher, alles komponierbar, alles wartbar. Das ist das React-Komponentenmodell – und TSX ist die Sprache, in der wir es ausdrücken.