7 TSX – React-Komponenten in TypeScript

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.

7.1 Was TSX wirklich ist

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.

7.2 Die Syntax: Ähnlich zu HTML, aber anders

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.

7.3 Komponenten: Eigene Tags definieren

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.

7.4 Props: Daten an Komponenten übergeben

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.

7.5 Komplexe Props: Objekte, Arrays, Funktionen

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.

7.6 Conditional Rendering: UI basierend auf Bedingungen

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.

7.7 Listen rendern: map() und keys

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

7.8 Event Handling: Interaktivität

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.

7.9 Komponenten verschachteln: Das Baukastensystem

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.

7.10 Ein vollständiges Beispiel: Todo-App

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.

7.11 TSX vs. JSX: Warum TypeScript?

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.

7.12 Häufige Fehler und wie man sie vermeidet

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.