React + TypeScript Crashkurs
Bevor wir uns in Next.js-spezifische Konzepte stürzen, brauchen wir eine solide Basis: React und TypeScript. Dieses Modul ist kein vollständiges React-Handbuch — es deckt genau die Konzepte ab, die du für den Rest des Tutorials benötigst. Als Rails-Entwickler wirst du vieles wiedererkennen: Komponentisierung, Datenfluss, Template-Syntax. Die Paradigmen sind vertraut, die Syntax ist neu.
1. JSX — HTML in JavaScript
JSX ist die Syntax, mit der du in React-Komponenten aussieht-wie-HTML schreibst. Der Compiler (Babel bzw. der TypeScript-Compiler) übersetzt JSX in JavaScript-Funktionsaufrufe. Das Ergebnis ist kein String wie bei ERB, sondern ein Objektbaum — React nennt das den Virtual DOM.
// Einfaches JSX-Element
const heading = <h1>Hallo Welt</h1>;
// Ausdrücke in geschweifte Klammern
const name = "Anna";
const greeting = <p>Hallo, {name}!</p>;
// Berechnungen und Methodenaufrufe funktionieren ebenfalls
const items = ["Apfel", "Banane", "Kirsche"];
const list = (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);Drei Dinge, die sich von HTML unterscheiden:
classNamestattclass—classist ein reserviertes Wort in JavaScript.- Selbstschließende Tags sind Pflicht —
<img />,<input />,<br />. Vergisst du den Slash, wirft der Compiler einen Fehler. - Genau ein Wurzelelement — eine Komponente darf nur ein äußerstes Element zurückgeben. Wenn du mehrere Elemente ohne echten Wrapper brauchst, nutze ein Fragment:
<>…</>.
// Fehler: zwei nebeneinanderliegende Elemente
// return <h1>Titel</h1><p>Text</p>;
// Korrekt: in Fragment einwickeln
return (
<>
<h1>Titel</h1>
<p>Text</p>
</>
);<%# ERB-Template: Ruby-Ausdruck in <%= ... %> %>
<h1>Hallo, <%= @user.name %>!</h1>
<ul>
<% @items.each do |item| %>
<li><%= item %></li>
<% end %>
</ul>// JSX: JavaScript-Ausdruck in { ... }
<h1>Hallo, {user.name}!</h1>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>Der wesentliche Unterschied: ERB produziert einen String zur Laufzeit auf dem Server. JSX beschreibt eine Baumstruktur, die React rendert — entweder auf dem Server (React Server Components) oder im Browser.
2. Komponenten — wiederverwendbare UI-Bausteine
Eine React-Komponente ist eine JavaScript-Funktion, die JSX zurückgibt. Zwei Regeln gelten immer:
- Großgeschriebener Name — nur dann erkennt React, dass es sich um eine Komponente handelt, nicht um ein natives HTML-Element.
<button>ist ein HTML-Button,<Button>ist deine eigene Komponente. - Reiner Rückgabewert — Komponenten sollten bei gleichen Eingaben das gleiche JSX produzieren (keine Seiteneffekte im Render-Pfad außer über Hooks, dazu gleich mehr).
// Eine einfache Komponente ohne Zustand
function NoteCard() {
return (
<div className="rounded-xl border border-slate-200 p-4 shadow-sm">
<h2 className="font-semibold text-slate-900">Meine erste Note</h2>
<p className="text-slate-600 text-sm mt-1">React ist wie Rails — nur anders.</p>
</div>
);
}
// Komponenten können andere Komponenten nutzen
function NoteListe() {
return (
<div className="flex flex-col gap-3">
<NoteCard />
<NoteCard />
</div>
);
}In Rails nutzt du Partials (_note_card.html.erb) für wiederverwendbare UI-Teile. Modernere Rails-Projekte setzen auf ViewComponent — eine Ruby-Klasse mit einem Template. React-Komponenten funktionieren sehr ähnlich wie ViewComponents: ein Objekt (Funktion) mit Eigenschaften (Props) und einer Render-Methode (der Funktionsrumpf).
3. Props — Daten an Komponenten übergeben
Props sind die Parameter einer Komponente. In TypeScript definierst du sie als Interface — das gibt dir Autovervollständigung und Typfehler beim Kompilieren statt zur Laufzeit.
// Props-Interface: beschreibt, was die Komponente erwartet
interface NoteCardProps {
title: string; // Pflichtfeld
body: string; // Pflichtfeld
createdAt: Date; // Pflichtfeld
pinned?: boolean; // Optional (das ? macht es optional)
onDelete?: () => void; // Optionale Callback-Funktion
}
function NoteCard({ title, body, createdAt, pinned = false, onDelete }: NoteCardProps) {
return (
<div className={`rounded-xl border p-4 ${pinned ? "border-indigo-400 bg-indigo-50" : "border-slate-200"}`}>
<div className="flex items-start justify-between">
<h2 className="font-semibold text-slate-900">{title}</h2>
{pinned && <span className="text-xs text-indigo-600 font-medium">Angeheftet</span>}
</div>
<p className="text-slate-600 text-sm mt-1">{body}</p>
<div className="flex items-center justify-between mt-3">
<time className="text-xs text-slate-400">{createdAt.toLocaleDateString("de-DE")}</time>
{onDelete && (
<button onClick={onDelete} className="text-xs text-red-500 hover:text-red-700">
Löschen
</button>
)}
</div>
</div>
);
}
// Verwendung
<NoteCard
title="React Props"
body="Props fließen immer von oben nach unten — von Eltern zu Kindern."
createdAt={new Date()}
pinned
onDelete={() => console.log("gelöscht")}
/>Props sind unveränderlich. Eine Komponente darf ihre eigenen Props nicht ändern — das ist das gleiche Prinzip wie Funktionsargumente in Ruby. Wenn sich etwas ändern soll, braucht es Zustand (useState) oder eine Callback-Funktion, die der Elternteil übergeben hat.
4. TypeScript Basics
TypeScript ist JavaScript mit Typen. Der Compiler prüft deine Typen beim Build — Laufzeitfehler durch falsche Typen fallen schon beim tsc auf. Hier sind die Grundbausteine, die du in diesem Tutorial brauchst:
// Primitive Typen
const title: string = "Hallo";
const count: number = 42;
const active: boolean = true;
// Arrays
const tags: string[] = ["react", "typescript"];
const ids: number[] = [1, 2, 3];
// Interfaces — beschreiben die Form eines Objekts
interface Note {
id: number;
title: string;
body: string;
createdAt: Date;
tags: string[];
}
// Type Aliases — oft austauschbar mit interfaces
type Status = "draft" | "published" | "archived"; // Union Type
// Optionales Chaining — kein NoMethodError mehr
const note: Note | null = null;
const len = note?.title.length; // undefined statt Fehler
const display = note?.title ?? "Kein Titel"; // Nullish Coalescing
// Generics — Typen als Parameter
function firstOf<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = firstOf(["a", "b"]); // TypeScript inferiert: string | undefinedDu musst nicht immer explizit tippen — TypeScript inferiert Typen aus Zuweisungen. const title = "Hallo" ist automatisch string. Explizite Typen brauchst du vor allem bei Funktionsparametern, Interfaces und dort, wo TypeScript nicht sicher schließen kann.
5. useState — Reaktiver Zustand
Zustand ist ein Wert, der sich über die Zeit ändern kann und bei jeder Änderung ein Re-Render auslöst. In React verwaltest du lokalen Zustand mit dem useState-Hook.
"use client"; // Wichtig! Hooks funktionieren nur in Client Components
import { useState } from "react";
function Counter() {
// useState gibt ein Tupel zurück: [aktueller Wert, Setter-Funktion]
const [count, setCount] = useState(0); // 0 ist der Anfangswert
return (
<div>
<p className="text-4xl font-bold text-indigo-600">{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>−</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}Wichtige Regel: Mutiere den Zustand niemals direkt. count++ oder setCount(count++) funktioniert nicht zuverlässig — nutze immer die Setter-Funktion. Bei komplexeren Updates (z.B. Arrays) verwendest du die Funktionsform des Setters:
// Korrekt: funktionale Aktualisierung — sicher bei asynchronen Updates
setCount((prev) => prev + 1);
// Korrekt: Array-Update ohne Mutation
setTags((prev) => [...prev, "neu"]);
// FALSCH: direktes Mutieren
count++; // React bemerkt die Änderung nicht
tags.push("neu"); // Gleicher FehlerProbiere den Counter live:
Nur Client Components haben Zustand. In Next.js sind Komponenten standardmäßig Server Components — sie laufen nur auf dem Server, haben keinen Zustand und können keine Event-Handler nutzen. Sobald du useState oder useEffect verwendest, musst du "use client" an den Anfang der Datei schreiben.
6. useEffect — Seiteneffekte
useEffect führt Code aus, nachdem die Komponente gerendert wurde. Typische Anwendungsfälle: DOM-Manipulation, Abonnements starten, oder externe APIs aufrufen.
"use client";
import { useState, useEffect } from "react";
function DocumentTitle({ title }: { title: string }) {
useEffect(() => {
// Läuft nach jedem Render, bei dem sich `title` geändert hat
document.title = title;
// Cleanup-Funktion: läuft bevor der nächste Effect ausgeführt wird
// und wenn die Komponente aus dem DOM entfernt wird
return () => {
document.title = "Next.js Tutorial";
};
}, [title]); // Dependency Array: Effect läuft nur wenn `title` sich ändert
return null;
}Das Dependency Array steuert, wann der Effect erneut läuft:
| Array | Wann läuft der Effect? |
|---|---|
| Kein Array | Nach jedem Render |
| [] (leer) | Einmalig nach dem ersten Render (wie componentDidMount) |
| [wert] | Wenn sich wert ändert |
Wichtig für Next.js: Nutze
useEffectfür Seiteneffekte im Browser (DOM, Timer, Event-Listener). Für Datenabruf bevorzuge immer Server Components — diese können direktasync/awaitverwenden und laufen nur auf dem Server, wo Datenbankzugriffe und API-Aufrufe effizienter und sicherer sind.
7. Controlled Inputs — Formulareingaben mit Zustand
In Rails verwaltet das Formular seinen eigenen Zustand — der Browser merkt sich, was der User eingetippt hat, bis das Formular abgeschickt wird. In React hast du die Wahl: unkontrollierte Eingaben (der Browser merkt sich den Wert) oder kontrollierte Eingaben, bei denen React der einzige Besitzer des Wertes ist.
Kontrollierte Eingaben sind in React der Standardweg, weil du den aktuellen Wert jederzeit im Zustand hast und darauf reagieren kannst:
"use client";
import { useState } from "react";
function SearchField() {
const [query, setQuery] = useState("");
return (
<div>
<input
type="text"
value={query} // Wert kommt aus dem Zustand
onChange={(e) => setQuery(e.target.value)} // Jede Änderung aktualisiert den Zustand
placeholder="Suche …"
/>
{/* Unmittelbarer Zugriff auf den aktuellen Wert */}
{query && <p>Du suchst nach: <strong>{query}</strong></p>}
</div>
);
}Das Muster ist immer dasselbe: value={zustand} und onChange={setter}. React kontrolliert, was im Input steht — daher der Name.
Gib deinen Namen ein …
Erstelle die Datei components/demos/M1Ampel.tsx. Implementiere eine Ampel, die per Klick auf einen Button durch die Phasen Rot → Gelb → Grün → Rot zyklisch wechselt.
Anforderungen:
"use client"am Anfang der DateiuseStatefür die aktuelle Phase — nutze einen Union-Type:type Phase = "rot" | "gelb" | "gruen"- Zeige drei Kreise übereinander an — der aktive leuchtet hell, die inaktiven sind gedimmt
- Ein Button darunter zeigt die nächste Phase an und löst den Wechsel aus
- Exportiere als default und named Export
Hinweise:
- Mit
["rot", "gelb", "gruen"]und dem Index des aktuellen Zustands kannst du die nächste Phase berechnen:(currentIndex + 1) % 3 - Tailwind-Farben für die Kreise:
bg-red-500,bg-yellow-400,bg-green-500für aktiv;bg-red-200,bg-yellow-200,bg-green-200für inaktiv
Im Capstone: DevNotes
Das Capstone-Projekt DevNotes — eine kleine Notizen-App — nutzt genau diese Konzepte in der Praxis. Die Aufteilung zwischen Server und Client Components ist dabei nicht willkürlich, sondern folgt einem klaren Muster:
NoteCard ist eine Server Component. Sie erhält fertige Daten als Props, rendert auf dem Server und sendet statisches HTML an den Browser. Kein JavaScript-Bundle, kein Hydration-Aufwand — schnell und einfach.
NoteForm hingegen ist eine Client Component. Sie nutzt useFormStatus aus react-dom, um den Ladezustand während einer Server Action anzuzeigen — der Submit-Button zeigt "Speichern…" während die Anfrage läuft. Das funktioniert nur im Browser, daher "use client".
Diese Aufteilung — Server Component für Anzeige, Client Component für Interaktion — ist das zentrale Muster in Next.js und kehrt in jedem weiteren Modul wieder.