Lern-MotorIn Komponenten denken

20 min · Stand 2026-08-29

In Komponenten denken

Warum baut man eine Seite aus Bausteinen — und wie fließen Daten hinein?

Im Artifact steht das ganze Cockpit in einer Datei: die Karten mit den Kennzahlen, die Kampagnen-Tabelle, die Zeitraum-Umschaltung, alles untereinander weg. Im Next-Projekt gibt es dafür den Ordner src/components, und darin je eine Datei pro Stück Oberfläche. Diese Lektion zeigt, was so ein Stück technisch ist, wie Daten hineinkommen und was beim Anzeigen in welcher Reihenfolge passiert.

Eine Karte statt einer Wand aus Zahlen

Eine Komponente ist eine ganz normale JavaScript-Funktion. Sie nimmt ein Objekt mit Werten entgegen und gibt zurück, wie das aussehen soll. Das Markup steht dabei direkt im Code — diese Schreibweise heißt JSX und ist HTML mit ein paar Abweichungen.

// src/components/KennzahlKarte.jsx
export function KennzahlKarte({ titel, wert, einheit }) {
  return (
    <div className="karte">
      <p className="titel">{titel}</p>
      <p className="wert">{wert} {einheit}</p>
    </div>
  );
}

Die Werte in den geschweiften Klammern sind die Stellen, an denen die Funktion einsetzt, was sie bekommen hat. Diese Werte heißen Props. Wer die Karte benutzt, schreibt sie wie Attribute hin:

<KennzahlKarte titel="Break-even-ROAS" wert={2.0} />
<KennzahlKarte titel="Werbekosten" wert={4000} einheit="" />

Zwei Karten, eine Funktion. Ändert sich das Aussehen, fasst du eine Datei an, und beide Karten ziehen mit. Das ist der Grund für die Aufteilung: Jede Änderung hat eine Stelle statt vieler.

JSX ist fast HTML. Das Attribut für die CSS-Klasse heißt aber , weil class in JavaScript schon als Schlüsselwort vergeben ist. Werte aus JavaScript stehen in , also wert={kampagne.roas}. Tags ohne Inhalt schließen sich selbst: aus <br> wird . Und jeder Eintrag, den ein map erzeugt, braucht ein -Attribut, damit React die Einträge zwischen zwei Durchgängen auseinanderhalten kann.

Groß- und Kleinschreibung ist egal.

Eine Zeile je Kampagne

Die Kampagnen kommen später als Liste von Objekten aus der Reporting-API. Aus einer Liste von Daten wird eine Liste von Zeilen mit map: Die Methode geht jeden Eintrag durch, ruft für ihn deine Funktion auf und sammelt die Rückgaben in einer neuen Liste. In JSX gibst du pro Eintrag ein Element zurück.

// src/components/KampagnenTabelle.jsx
export function KampagnenTabelle({ kampagnen }) {
  return (
    <tbody>
      {kampagnen.map((k) => (
        <tr key={k.id}>
          <td>{k.name}</td>
          <td>{k.roas.toFixed(1)}</td>
        </tr>
      ))}
    </tbody>
  );
}

Das key ist keine Zierde. React vergleicht bei jeder Neuanzeige die alte Liste mit der neuen und braucht dafür eine Kennung je Eintrag, die zum Eintrag gehört und nicht zu seiner Position.

Das Muster selbst hat mit React nichts zu tun, es ist gewöhnliches JavaScript. Deshalb kannst du es hier ohne React üben: Das folgende Feld führt reines JavaScript in deinem Browser aus, es gibt kein JSX und keine Komponenten. Statt Zeilen-Elementen baust du Zeilen-Texte — der Weg von den Daten zur Liste ist derselbe.

Aus Kampagnen werden Zeilen

Jede Zeile soll „Brand Search: ROAS 4.0" heißen — Name, Doppelpunkt, ROAS mit einer Nachkommastelle. Der ROAS ist Umsatz durch Kosten; toFixed(1) macht die Nachkommastelle.

— noch keine Ausgabe —

Reines JavaScript, ausgeführt in einem Worker in deinem Browser. Endlosschleifen werden nach 3 Sekunden abgeräumt.

Prospecting kommt dabei auf 1,5: Auf jeden Euro Werbekosten kommen anderthalb Euro Umsatz zurück. Ob das reicht, hängt an einer Schwelle, die im letzten Step dazukommt.

Die Konsole warnt: „Each child in a list should have a unique key prop." Die Tabelle wird trotzdem angezeigt. Was tust du?

Props kommen von außen, State bleibt drinnen

Bis hierhin fließen die Daten in eine Richtung: von der Seite in die Komponenten. Props sind dabei Lesestoff — die Komponente bekommt sie beim Aufruf und gibt sie nicht zurück. Was sich durch einen Klick ändern soll, braucht etwas anderes: State, den eigenen Zustand einer Komponente.

'use client';

import { useState } from 'react';

export function ZeitraumWahl({ start, onWahl }) {
  const [zeitraum, setZeitraum] = useState(start);

  return (
    <div>
      {['7d', '30d', '90d'].map((z) => (
        <button
          key={z}
          className={z === zeitraum ? 'aktiv' : ''}
          onClick={() => { setZeitraum(z); onWahl(z); }}
        >
          {z}
        </button>
      ))}
    </div>
  );
}

useState gibt dir zwei Dinge: den aktuellen Wert und eine Funktion, die ihn setzt. Aufgerufen wird die Setz-Funktion, direkt zuweisen kannst du dem Wert nichts. Der Grund ist der Ablauf: Erst durch den Aufruf weiß React, dass sich etwas geändert hat, und lässt die Komponente noch einmal laufen — nur diese, nicht die ganze Seite.

Die KennzahlKarte bekommt titel und wert als Props. Ein Klick auf die Karte soll den Wert des Vormonats zeigen. Wer ändert den Wert?

Sitzt das?

Was in welcher Reihenfolge passiert

Die Seite selbst ist auch eine Komponente — nur eine, die auf dem Server läuft und deshalb Daten holen darf. Sie ruft die API, rechnet und gibt die fertigen Zahlen als Props weiter.

// src/app/page.js — läuft auf dem Server
import { holeKampagnen } from '@/lib/brain';
import { beRoas } from '@/lib/kennzahlen';
import { KennzahlKarte } from '@/components/KennzahlKarte';
import { KampagnenTabelle } from '@/components/KampagnenTabelle';

export default async function Cockpit() {
  const kampagnen = await holeKampagnen('30d');
  const schwelle = beRoas({ wareneinsatz: 35, versand: 10, gebuehren: 5 });

  return (
    <main>
      <KennzahlKarte titel="Break-even-ROAS" wert={schwelle.toFixed(1)} />
      <KampagnenTabelle kampagnen={kampagnen} />
    </main>
  );
}

Zwischen diesem Aufruf und dem ersten möglichen Klick liegen mehrere Stationen. Sie zu kennen, spart später Sucherei: Wenn eine Zahl fehlt, ist die Frage immer, an welcher Station sie verloren ging.

Jemand öffnet das Cockpit. Bring die Stationen in die Reihenfolge, in der sie ablaufen.

  1. 1Die Seite läuft auf dem Server und holt die Kampagnen über lib/brain.js.
  2. 2Sie rechnet mit lib/kennzahlen.js die Kennzahlen aus und gibt sie als Props weiter.
  3. 3Jede Komponentenfunktion läuft einmal durch und liefert ihr Markup.
  4. 4Der Browser bekommt fertiges HTML — die Zahlen stehen da, bevor irgendetwas anklickbar ist.
  5. 5React übernimmt im Browser die Client-Komponenten; erst jetzt reagiert die Zeitraum-Wahl auf Klicks.
Mit den Pfeilen sortieren, dann prüfen.

Rechnen gehört nicht in die Karte

Der Break-even-ROAS aus der Seite oben ist die Schwelle, ab der eine Kampagne ihre eigenen Kosten deckt. Er hängt an drei Quoten vom Umsatz: Wareneinsatz, Versand und Zahlungsgebühren. Was danach übrig bleibt, ist die Marge, und der Kehrwert der Marge ist der Break-even-ROAS.

Diese Rechnung steht in src/lib/kennzahlen.js und nicht in der Karte. Sie hat mit Anzeigen nichts zu tun, sie gilt auch woanders, und du kannst sie im Terminal mit node durchrechnen, ohne den Browser zu öffnen. Genau das machst du hier.

Die Schwelle ausrechnen

Die Quoten sind Prozentwerte vom Umsatz: 35 % Wareneinsatz, 10 % Versand, 5 % Zahlungsgebühren. Ergänze marge so, dass beRoas den Break-even-ROAS liefert — 1 geteilt durch den Anteil, der vom Umsatz übrig bleibt. Erwartet wird 2.

— noch keine Ausgabe —

marge ist ein Anteil zwischen 0 und 1, die Quoten stehen in Prozent.

Mit der Schwelle 2,0 sortiert sich die Tabelle von selbst: Brand Search mit 4,0 und Retargeting mit 3,0 tragen sich, Prospecting mit 1,5 nicht. Die Karte zeigt diese Zahlen nur an, ausgerechnet werden sie in lib/kennzahlen.js.

Damit steht die Oberfläche: Karten, eine Tabelle, eine Umschaltung, und die Rechnung daneben. Was noch fehlt, sind die echten Zahlen. In der nächsten Lektion holt lib/brain.js sie von der Reporting-API — mit dem Key an der Stelle, an die er gehört.

Häufige Fragen

Woher weiß ich, ob eine Komponente „use client" braucht?

Sobald sie useState, useEffect oder einen Handler wie onClick benutzt. Alles andere bleibt ohne die Zeile und läuft auf dem Server. Wenn du unsicher bist, lass sie weg: Next meldet beim Start deutlich, wenn die Zeile fehlt.

Eine Datei je Komponente, auch bei fünf Zeilen?

Nicht zwingend. Was nur an einer Stelle vorkommt, darf in derselben Datei stehen. Eigene Dateien lohnen sich, sobald etwas an zwei Stellen benutzt wird oder du es einzeln beschreiben willst — auch für Claude Code sind kleine benannte Dateien leichter zu treffen als ein langer Block.

Muss die Datei .jsx heißen oder reicht .js?

Beides läuft, Next unterscheidet nicht. Die Endung .jsx sagt beim Überfliegen des Ordners, dass Markup drin ist — mehr ist es nicht.

Warum heißen Komponenten mit großem Anfangsbuchstaben?

JSX unterscheidet daran, was gemeint ist: klein geschrieben ist ein HTML-Element, groß geschrieben deine Funktion. Eine Komponente namens kennzahlKarte landet als unbekanntes Element im Markup, statt dass deine Funktion aufgerufen wird.

Kann ich das Artifact-Layout so lassen und nur den Code aufteilen?

Ja. Die Aufteilung ändert nichts am Aussehen — dieselben Elemente, dieselben Klassen, nur auf mehrere Dateien verteilt. Das Aussehen umzubauen ist ein eigener Schritt, den du später machen kannst.