Von HTML zu React
In diesem Kapitel baust du eine einfache Counter-App — erst mit purem HTML, CSS und JavaScript, dann schrittweise mit React. So siehst du genau, welche Probleme React löst und wie es unter der Haube funktioniert.
Ausgangspunkt
Abschnitt betitelt „Ausgangspunkt“Erstelle drei Dateien in einem neuen Ordner:
index.html
<!DOCTYPE html><html lang="de"> <head> <meta charset="UTF-8" /> <title>Counter</title> <link rel="stylesheet" href="style.css" /> <script type="module" src="main.js"></script> </head> <body> <div id="app"> <section> <h1>Counter</h1> <div> <p class="display"> Stand: <span>0</span> </p> <button>+</button> </div> </section> </div> </body></html>style.css
body { font-family: system-ui, sans-serif; display: grid; place-items: center; min-height: 100vh; margin: 0;}
section { text-align: center;}
.display { font-size: 1.5rem;}
button { font-size: 1.5rem; padding: 0.25rem 1rem; cursor: pointer;}main.js
console.log("Your starting point — enjoy the ride!");Öffne index.html im Browser. Du siehst den Counter, aber der Button tut noch nichts.
Dev-Server
Abschnitt betitelt „Dev-Server“Das <script type="module"> in der HTML-Datei sorgt dafür, dass der Browser die JavaScript-Datei als ES-Modul lädt. Das funktioniert aber nicht mit file:// — du brauchst einen HTTP-Server.
Zwei Optionen:
npx serve— ein einfacher statischer Servernpx vite— ein Build-Tool mit Dev-Server, das wir ab Schritt 3 sowieso brauchen
Empfehlung: Starte gleich mit Vite, dann musst du später nicht wechseln:
npx viteSchritt 1: Interaktivität mit JavaScript
Abschnitt betitelt „Schritt 1: Interaktivität mit JavaScript“Der Counter soll beim Klick auf den Button hochzählen. Dafür brauchen wir eine Verbindung zwischen HTML und JavaScript. Die Brücke: ID-Attribute.
Ergänze in index.html zwei IDs:
<span id="count">0</span><!-- ... --><button id="increment">+</button>Jetzt kann JavaScript die Elemente finden. Ersetze den Inhalt von main.js:
const countElement = document.querySelector("#count");const button = document.querySelector("#increment");
let count = 0;
button.addEventListener("click", () => { count++; countElement.textContent = count;});Der Counter funktioniert! Aber schau genau hin:
- Die UI-Struktur lebt in
index.html - Die Logik lebt in
main.js - Verbunden sind sie nur durch die Strings
"#count"und"#increment"
Das ist fragil. Wenn jemand die ID im HTML ändert, bemerkt JavaScript das erst zur Laufzeit — kein Fehler beim Speichern, kein Fehler beim Laden, erst beim Klick passiert nichts. Bei einer kleinen App wie dieser kein Drama, aber stell dir eine Seite mit Hunderten von Elementen vor.
Schritt 2: UI mit JavaScript aufbauen
Abschnitt betitelt „Schritt 2: UI mit JavaScript aufbauen“Was wäre, wenn UI und Logik am selben Ort leben? Dann können sie nicht mehr auseinanderlaufen.
Reduziere index.html auf das Minimum:
<!DOCTYPE html><html lang="de"> <head> <meta charset="UTF-8" /> <title>Counter</title> <link rel="stylesheet" href="style.css" /> <script type="module" src="main.js"></script> </head> <body> <div id="app"></div> </body></html>Baue die gesamte UI in main.js mit der DOM-API auf:
const container = document.getElementById("app");
let count = 0;
// Struktur aufbauenconst section = document.createElement("section");
const h1 = document.createElement("h1");h1.textContent = "Counter";
const div = document.createElement("div");
const p = document.createElement("p");p.className = "display";p.textContent = "Stand: ";
const span = document.createElement("span");span.textContent = count;
const button = document.createElement("button");button.textContent = "+";button.addEventListener("click", () => { count++; span.textContent = count;});
// Zusammensetzenp.appendChild(span);div.appendChild(p);div.appendChild(button);section.appendChild(h1);section.appendChild(div);container.appendChild(section);UI und Logik leben jetzt zusammen — keine ID-Strings mehr, kein Auseinanderlaufen. Aber:
- ~30 Zeilen für eine winzige UI
- Die DOM-API (
createElement,appendChild,textContent) ist wortreich - Die Baumstruktur der UI ist im Code kaum erkennbar — du siehst nicht auf einen Blick, was Kind von was ist
Deklarativer Ansatz mit der DOM-API
Abschnitt betitelt „Deklarativer Ansatz mit der DOM-API“Es gibt noch ein weiteres Problem: Der Click-Handler ändert direkt span.textContent. Das funktioniert hier, aber bei komplexerer UI musst du genau wissen, welches DOM-Element du anfassen musst. Besser wäre es, die gesamte UI als Funktion des aktuellen Zustands zu beschreiben — und bei jeder Änderung neu zu rendern.
Refaktoriere main.js zu einer render()-Funktion:
const container = document.getElementById("app");
let count = 0;
function render() { // Alten Inhalt entfernen container.innerHTML = "";
// Struktur neu aufbauen const section = document.createElement("section");
const h1 = document.createElement("h1"); h1.textContent = "Counter";
const div = document.createElement("div");
const p = document.createElement("p"); p.className = "display"; p.textContent = "Stand: ";
const span = document.createElement("span"); span.textContent = count;
const button = document.createElement("button"); button.textContent = "+"; button.addEventListener("click", () => { count++; render(); });
// Zusammensetzen p.appendChild(span); div.appendChild(p); div.appendChild(button); section.appendChild(h1); section.appendChild(div); container.appendChild(section);}
render();Die Struktur ist jetzt klar: render() beschreibt die UI basierend auf count, und bei jedem Klick wird alles neu gerendert. Aber der Browser ersetzt dabei den kompletten DOM-Baum — innerHTML = "" löscht alles, dann wird alles von Grund auf neu aufgebaut. Bei einer kleinen App kein Problem, aber bei einer großen Seite wäre das langsam und würde z.B. den Fokus-Zustand von Eingabefeldern zerstören.
Schritt 3: React-Elemente
Abschnitt betitelt „Schritt 3: React-Elemente“React nutzt genau dieses render()-Muster — aber löst das Performance-Problem. Statt document.createElement und appendChild beschreibst du die UI deklarativ als verschachtelten Baum von Elementen. React vergleicht dann den neuen Baum mit dem vorherigen und aktualisiert nur die DOM-Knoten, die sich tatsächlich geändert haben.
Initialisiere ein npm-Projekt und installiere React:
npm init -ynpm install react react-domWarum zwei Pakete? React ist in zwei Teile aufgeteilt:
react— der Kern. Hier lebencreateElementund später Hooks wieuseState. Dieses Paket weiß nichts über den Browser oder den DOM.react-dom— die Brücke zum Browser-DOM. Hier stecktcreateRootund die Logik, die den Elementbaum mit dem echten DOM abgleicht.
Diese Trennung ist kein Zufall: Derselbe React-Kern funktioniert auch mit anderen Zielen — z.B. react-native für mobile Apps oder react-three-fiber für 3D-Szenen. Du tauschst nur die Render-Schicht aus.
Da wir Vite als Dev-Server nutzen, werden die import-Anweisungen automatisch aufgelöst — kein weiteres Setup nötig.
React.createElement
Abschnitt betitelt „React.createElement“Die zentrale Funktion ist React.createElement:
| Parameter | Bedeutung | Beispiel |
|---|---|---|
typ | HTML-Tag oder Komponente | "h1", "button" |
props | Attribute als Objekt (oder null) | { className: "display" } |
...kinder | Kindelemente oder Text | "Counter", weitere Elemente |
Ersetze den Inhalt von main.js:
import React from "react";import { createRoot } from "react-dom/client";
const container = document.getElementById("app");const root = createRoot(container);
let count = 0;
function render() { const element = React.createElement( "section", null, React.createElement("h1", null, "Counter"), React.createElement( "div", null, React.createElement( "p", { className: "display" }, "Stand: ", React.createElement("span", null, count) ), React.createElement( "button", { onClick: () => { count++; render(); }, }, "+" ) ) );
root.render(element);}
render();Was hat sich geändert?
- Deklarativ: Du beschreibst was angezeigt werden soll, nicht wie die DOM-Knoten zusammengesteckt werden
- Baumstruktur: Die Verschachtelung der
createElement-Aufrufe spiegelt die UI-Hierarchie wider - Kein
appendChild: React kümmert sich selbst darum, den DOM zu aktualisieren - Nur die Diffs: Anders als unser
innerHTML = ""-Ansatz aus Schritt 2 ersetzt React nicht den ganzen Baum. Es vergleicht den neuen Elementbaum mit dem vorherigen und ändert nur das, was sich unterscheidet — hier also nur den Text im<span>
Schritt 4: JSX
Abschnitt betitelt „Schritt 4: JSX“React.createElement macht die Struktur sichtbar, aber schön lesen lässt es sich nicht. Hier kommt JSX ins Spiel.
JSX ist eine Syntaxerweiterung für JavaScript, die der Browser nicht versteht. Bevor der Code ausgeführt werden kann, muss ein Transpiler ihn in reguläres JavaScript — also React.createElement-Aufrufe — umwandeln. Ursprünglich übernahm das Babel, der erste weit verbreitete JavaScript-Transpiler. Heute erledigen das moderne Build-Tools wie Vite (intern über esbuild) deutlich schneller.
Benenne main.js in main.jsx um und passe das Script-Tag in index.html an:
<script type="module" src="main.jsx"></script>Vite erkennt die .jsx-Endung und transpiliert JSX automatisch in React.createElement-Aufrufe.
Schreibe den Counter in main.jsx mit JSX:
import React from "react";import { createRoot } from "react-dom/client";
const container = document.getElementById("app");const root = createRoot(container);
let count = 0;
function render() { const element = ( <section> <h1>Counter</h1> <div> <p className="display"> Stand: <span>{count}</span> </p> <button onClick={() => { count++; render(); }}>+</button> </div> </section> );
root.render(element);}
render();Das sieht fast aus wie das HTML, mit dem wir angefangen haben — nur dass die Logik direkt dabei ist.
JSX = Funktionsaufrufe
Abschnitt betitelt „JSX = Funktionsaufrufe“JSX ist kein HTML. Es ist syntaktischer Zucker für React.createElement. Der Compiler (hier: Vite) wandelt jedes JSX-Element in einen Funktionsaufruf um:
<h1>Counter</h1>wird zu:
React.createElement("h1", null, "Counter")Auch bei verschachtelten Elementen:
<p className="display"> Stand: <span>{count}</span></p>wird zu:
React.createElement( "p", { className: "display" }, "Stand: ", React.createElement("span", null, count))JSX ist also nichts Magisches — es macht Schritt 3 nur lesbarer. Und weil JSX letztlich JavaScript ist, kannst du die volle Kraft von JavaScript nutzen: Variablen, Bedingungen, Schleifen, Funktionsaufrufe — alles direkt in deiner UI-Beschreibung.
Du hast jetzt gesehen, wie React aus einem konkreten Problem entsteht: UI und Logik zusammenbringen, ohne die Lesbarkeit zu verlieren. Im nächsten Abschnitt — UI Beschreiben — lernst du, wie du deine UI in wiederverwendbare Komponenten aufteilst, welche JSX-Regeln gelten und wie du JavaScript-Ausdrücke in JSX einbettest.