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:

  • className statt classclass ist 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>
  </>
);
Rails-Analogie
Ruby on Rails
<%# ERB-Template: Ruby-Ausdruck in <%= ... %>  %>
<h1>Hallo, <%= @user.name %>!</h1>
<ul>
<% @items.each do |item| %>
  <li><%= item %></li>
<% end %>
</ul>
Next.js
// 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:

  1. 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.
  2. 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>
  );
}
Rails-Analogie

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 | undefined

Du 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 Fehler

Probiere den Counter live:

useState — CounterKlick auf + und − um den Zustand zu ändern
0

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 useEffect für Seiteneffekte im Browser (DOM, Timer, Event-Listener). Für Datenabruf bevorzuge immer Server Components — diese können direkt async/await verwenden 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.

Controlled Input — Live-GreetingTippe deinen Namen ein

Gib deinen Namen ein …


Aufgabe: Ampelkomponente

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 Datei
  • useState fü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-500 für aktiv; bg-red-200, bg-yellow-200, bg-green-200 fü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.