JSX genau: Die Regeln hinter dem Markup
Ein präziser Blick auf JSX – was der Compiler daraus macht, warum es Ausdrücke und keine Anweisungen sind, wie Attribute, Kinder, Bedingungen und Listen wirklich funktionieren und an welchen Stellen Anfänger reihenweise stolpern.
JSX genau: Die Regeln hinter dem Markup
Im React-Überblick habe ich JSX in einem Satz abgehandelt: Es sieht aus wie HTML, ist aber nur JavaScript. Das stimmt, reicht aber nicht, um die Fehler zu vermeiden, die im Alltag entstehen. Dieser Beitrag ist der versprochene Detail-Deep-Dive zu JSX. Er ist bewusst kleinteilig: Ich gehe die Regeln der Reihe nach durch, jede mit einem winzigen Beispiel und, wo es zählt, mit dem Fehler, den man macht, wenn man die Regel nicht kennt. Wer JSX einmal genau verstanden hat, liest fremden React-Code danach anders.
Was der Compiler wirklich daraus macht
JSX ist keine Sprachfunktion von JavaScript. Kein Browser versteht es. Bevor der Code läuft, übersetzt ein Compiler – heute meist der Babel- oder SWC-Schritt in Vite oder Next.js – jedes JSX-Element in einen normalen Funktionsaufruf. Diese eine Tatsache erklärt fast alle Regeln, die danach kommen.
Früher war das Ziel dieser Übersetzung React.createElement. Seit React 17 gibt es den automatischen Transform, der stattdessen eine Funktion aus react/jsx-runtime einsetzt, sodass man React nicht mehr in jede Datei importieren muss. Am Ergebnis ändert das nichts Grundsätzliches: Aus dem Markup wird ein Aufruf, und aus dem Aufruf ein einfaches Objekt.
const el = <button className="primär" onClick={handleClick}>Speichern</button>;
wird sinngemäß zu:
const el = jsx("button", { className: "primär", onClick: handleClick, children: "Speichern" });
Das zurückgegebene Objekt beschreibt nur, was gerendert werden soll – es ist kein DOM-Knoten, es passiert noch nichts im Browser. React nimmt dieses Objekt später und erzeugt oder aktualisiert daraus das echte DOM. Man nennt so ein Beschreibungsobjekt ein React-Element. Wenn man das im Kopf hat, wird verständlich, warum ein Tippfehler im JSX oft als ganz normaler JavaScript-Fehler erscheint: Es ist ja am Ende ein Funktionsaufruf.
Ausdruck, nicht Anweisung
Die wichtigste Regel für den geschweiften Klammerinhalt lautet: Zwischen { und } steht ein Ausdruck, keine Anweisung. Ein Ausdruck ist etwas, das einen Wert ergibt – nutzer.name, a + b, preis > 0 ? "ok" : "nein", ein Funktionsaufruf. Eine Anweisung ist etwas, das etwas tut, aber keinen Wert liefert – if, for, while, switch. Anweisungen sind in den Klammern nicht erlaubt.
// funktioniert – ein Ausdruck
<p>{nutzer ? nutzer.name : "Gast"}</p>
// funktioniert nicht – if ist eine Anweisung
<p>{if (nutzer) { nutzer.name }}</p>
Deshalb greift man in JSX so oft zum ternären Operator ? : und zum logischen &&: Sie sind die Ausdrucks-Varianten von if. Wer eine echte Verzweigung mit mehreren Zweigen braucht, zieht sie vor das return und arbeitet dort mit ganz normalem if:
function Status({ zustand }) {
let text;
if (zustand === "offen") text = "Offen";
else if (zustand === "bezahlt") text = "Bezahlt";
else text = "Unbekannt";
return <span>{text}</span>;
}
Das ist kein Umweg, sondern die saubere Trennung: Logik oben, Darstellung unten. Ein häufiger Anfängerreflex ist, immer komplexere Ternäroperatoren ineinander zu schachteln, bis niemand sie mehr liest. Sobald zwei Ebenen erreicht sind, gehört die Entscheidung nach oben.
Attribute sind Props, und sie heißen anders als in HTML
JSX-Attribute sehen aus wie HTML-Attribute, sind aber die Eigenschaften des erzeugten Objekts – also Props. Weil die Zielsprache JavaScript ist und nicht HTML, folgen sie den Regeln von JavaScript, und an ein paar Stellen weicht das sichtbar von HTML ab.
Zwei Namen sind reserviert, weil class und for in JavaScript Schlüsselwörter sind. Deshalb heißt das Klassenattribut className und das Label-Attribut htmlFor:
<label htmlFor="email" className="feld-label">E-Mail</label>
<input id="email" />
Alle mehrteiligen HTML-Attribute werden in camelCase geschrieben: tabindex wird zu tabIndex, readonly zu readOnly, maxlength zu maxLength. Event-Handler heißen onClick, onChange, onSubmit – großes Binnen-C – und bekommen eine Funktion, keinen String:
// richtig – eine Funktionsreferenz
<button onClick={speichern}>Speichern</button>
// falsch – ruft speichern SOFORT beim Rendern auf und übergibt den Rückgabewert
<button onClick={speichern()}>Speichern</button>
Der zweite Fall ist einer der häufigsten Einsteigerfehler. onClick={speichern()} führt speichern beim Rendern aus und hängt dessen Ergebnis an den Handler. Wenn man Argumente übergeben will, wickelt man den Aufruf in eine Pfeilfunktion: onClick={() => speichern(id)}.
Werte gibt man in geschweiften Klammern, feste Strings in Anführungszeichen. Ein Attribut ohne Wert ist implizit true:
<input type="text" value={wert} disabled />
// disabled ist gleichbedeutend mit disabled={true}
Wie Werte im Inhalt gerendert werden – und was verschwindet
Was React aus einem Ausdruck im Inhalt macht, hängt vom Typ des Werts ab, und hier lauert eine ganze Reihe kleiner Überraschungen. Strings und Zahlen werden als Text ausgegeben. Arrays werden Element für Element gerendert. Aber null, undefined, true und false rendern nichts – sie verschwinden spurlos. Genau das macht bedingtes Rendern erst möglich.
<div>
{nachricht} {/* string oder zahl -> wird angezeigt */}
{null} {/* nichts */}
{istAdmin && <Panel/>} {/* Panel nur wenn istAdmin wahr ist */}
</div>
Der Klassiker-Fehler steckt im &&. Es rendert das rechte Element nur, wenn die linke Seite wahr ist – aber es gibt die linke Seite zurück, wenn sie falsch ist. Bei Booleans ist das harmlos, weil false verschwindet. Bei einer Zahl nicht:
// Falle: wenn anzahl 0 ist, erscheint eine 0 im UI, kein Nichts
{anzahl && <Liste />}
// richtig: erst zu einem Boolean machen
{anzahl > 0 && <Liste />}
Die 0 ist eben keine false, sondern eine Zahl, und Zahlen werden gerendert. Wer schon einmal eine einsame 0 in seiner Oberfläche gesucht hat, kennt diesen Fehler. Die Regel dahinter ist einfach: Vor einem && steht am besten ein echter Wahrheitswert, kein Wert, der zufällig falsy ist.
Ein Element, ein Wurzelknoten – und Fragmente
Weil ein JSX-Ausdruck am Ende ein einzelnes Objekt ergibt, muss eine Komponente genau ein Wurzelelement zurückgeben. Zwei Elemente nebeneinander ohne Klammerung sind ein Syntaxfehler. Man könnte alles in ein <div> wickeln, aber das erzeugt einen zusätzlichen, oft unerwünschten Knoten im DOM. Dafür gibt es das Fragment – eine Klammer, die im DOM nichts hinterlässt:
function Zeile() {
return (
<>
<td>Name</td>
<td>Betrag</td>
</>
);
}
Die Kurzschreibweise <>...</> reicht fast immer. Nur wenn das Fragment selbst ein key braucht – etwa als direktes Kind einer Liste – muss man die lange Form <Fragment key={id}>...</Fragment> verwenden, weil die Kurzform keine Attribute annimmt.
Die runden Klammern um mehrzeiliges JSX nach return sind übrigens keine Kosmetik gegen Einrückung, sondern ein Schutz vor einer JavaScript-Falle: Steht das erste Element in einer neuen Zeile direkt nach return, fügt JavaScript automatisch ein Semikolon ein, und die Funktion gibt undefined zurück. Die Klammer verhindert das.
Listen und Keys: die am meisten unterschätzte Regel
Eine Liste rendert man, indem man ein Array mit map in ein Array von Elementen verwandelt. Jedes dieser Elemente braucht ein key-Attribut. Der Key ist kein Deko-Detail – er ist die Information, an der React beim nächsten Rendern erkennt, welches alte Element welchem neuen entspricht.
<ul>
{rechnungen.map((r) => (
<li key={r.id}>{r.nummer}</li>
))}
</ul>
Der Key muss zwei Eigenschaften haben: Er muss unter den Geschwistern eindeutig sein, und er muss stabil sein, also bei denselben Daten immer gleich. Deshalb nimmt man eine fachliche ID, keinen zufälligen Wert und – das ist der wunde Punkt – möglichst nicht den Array-Index.
Der Index als Key funktioniert scheinbar, solange sich die Liste nie ändert. Sobald man aber vorne etwas einfügt, löscht oder sortiert, verschieben sich die Indizes, und React ordnet den alten Zuständen die falschen Elemente zu. Das äußert sich in gespenstischen Fehlern: Ein Eingabefeld behält seinen Text, obwohl die Zeile eine ganz andere sein sollte; eine Checkbox bleibt angehakt, obwohl das zugehörige Element gelöscht wurde. Der Index beschreibt eine Position, der Key soll aber eine Identität beschreiben – und Position und Identität sind zwei verschiedene Dinge. Nur wenn eine Liste garantiert statisch ist und nie umsortiert wird, ist der Index als Key vertretbar.
Spread, Kinder und die kleinen Fallstricke am Rand
Props kann man einzeln setzen oder mit dem Spread-Operator aus einem Objekt durchreichen. Das ist bequem, verdient aber Vorsicht:
const props = { type: "text", value: wert, onChange: aendern };
<input {...props} />
Sauber ist das, wenn man genau weiß, was im Objekt steckt. Unsauber wird es, wenn man ein ganzes fremdes Objekt blind hineinspreizt und damit unbemerkt Attribute setzt, die man gar nicht wollte. Kommt nach dem Spread noch ein einzelnes Attribut, gewinnt das letzte – <input {...props} value={anders} /> überschreibt das value aus dem Objekt.
Der Inhalt zwischen öffnendem und schließendem Tag landet in der besonderen Prop children. Eine Komponente, die children rendert, weiß nichts über ihren Inhalt und ist gerade deshalb überall einsetzbar. Man kann children auch explizit als Attribut übergeben, was aber selten schöner ist als die natürliche Schachtelung.
Ein paar weitere Details, die immer wieder auffallen: Inline-Styles sind in JSX ein Objekt, keine CSS-Zeichenkette, und ihre Schlüssel stehen in camelCase – <div style={{ backgroundColor: "red", fontSize: 14 }} />, mit doppelten Klammern, weil das äußere Paar den Ausdruck einleitet und das innere das Objekt ist. Kommentare innerhalb von JSX schreibt man als Ausdruck: {/* so */}. Und rohes HTML aus einem String setzt man nur über dangerouslySetInnerHTML ein – der sperrige Name ist Absicht, denn genau hier entstehen Cross-Site-Scripting-Lücken, wenn der String aus einer fremden Quelle stammt. Normaler Text in JSX wird von React automatisch escaped und ist deshalb sicher; dieser eine Weg umgeht diesen Schutz bewusst.
Warum sich die Genauigkeit lohnt
All diese Regeln folgen aus einem einzigen Gedanken: JSX ist eine Schreibweise für Funktionsaufrufe, die ein Beschreibungsobjekt erzeugen. Die reservierten Namen, die Ausdrucksregel, das Verschwinden von null und false, die 0-Falle, der Zwang zu einem Wurzelknoten, die Bedeutung des Keys – nichts davon ist Willkür, alles ergibt sich daraus, dass am Ende JavaScript läuft und nicht HTML.
Wer JSX so liest, hört auf, gegen die Sprache zu arbeiten. Man setzt className nicht mehr aus Versehen als class, man macht aus einer Zahl vor dem && einen echten Vergleich, man vergibt Keys aus fachlichen IDs statt aus Indizes – nicht weil ein Linter meckert, sondern weil man versteht, was der Compiler daraus macht. Zu den einzelnen Bausteinen, die hier nur gestreift wurden – der Datenfluss über Props, die Frage nach dem Zustandsbesitz, das Rendern auf dem Server –, gibt es eigene Beiträge. JSX ist der Anfang von allem, und genau deshalb lohnt es sich, hier genau zu sein.
Weiterführende Quellen
- Übungsrepository introduction-react (JSX-Kapitel und Beispiele): https://github.com/mikebild/introduction-react
- React, JSX-Markup schreiben: https://react.dev/learn/writing-markup-with-jsx
- React, JavaScript in JSX mit geschweiften Klammern: https://react.dev/learn/javascript-in-jsx-with-curly-braces
- React, Listen rendern und Keys: https://react.dev/learn/rendering-lists
- React, der neue JSX-Transform: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
Kommentare
Kommentar schreiben