Die erste Komponente
Kurz gesagt: Eine React-Komponente ist eine Funktion, die JSX zurückgibt: Sie beschreibt, wie der Bildschirm für die Daten aussieht, und React ändert auf der Seite nur den Unterschied.
Problem
Das Feld verliert den Fokus
Das Modul „JavaScript in der Seite“ endete mit einem Warenkorb, den ein einziges render() aus den Daten zeichnet. Hier ruft jede Änderung des Mengenfelds render() auf. Der Kunde möchte 15 Flaschen Milch: Er klickt ins Feld, löscht die 2 und tippt 15. Probier es aus.
Konsole
Nach dem Löschen ist das Feld leer und hat keinen Fokus mehr: Die 1 und die 5 gehen ins Leere. render() hat das Feld schon beim ersten Tastendruck durch ein neues ersetzt. Das neue Feld sieht gleich aus, ist aber ein anderes Element, und der Fokus lag auf dem alten. Einzelne Stellen von Hand anzufassen, vergisst Stellen. Alles neu zu zeichnen, verliert den Fokus. Es gibt einen dritten Weg.
Die Idee
Die Oberfläche als Funktion der Daten
render() hatte eine gute Idee: Der Bildschirm wird nur aus den Daten gebaut. Bei denselben Daten ist der Bildschirm derselbe, egal, was vorher darauf stand. Der Fehler lag im letzten Schritt: render() entfernt und erzeugt die Elemente selbst.
Die Arbeit wird deshalb in zwei Teile geteilt:
- Du schreibst eine Funktion, die sagt, wie der Bildschirm für diese Daten aussieht. Sie fasst die Seite nicht an; sie gibt eine Beschreibung zurück: Hier ist ein
<main>, darin ein<h1>mit dem Text „Der Warenkorb“, dann ein Feld mit dem Wert 2. - Die Bibliothek hält das echte DOM. Beim ersten Mal baut sie es aus der Beschreibung. Bei jeder Änderung der Daten nimmt sie die neue Beschreibung, vergleicht sie mit der alten und ändert auf der Seite nur den Unterschied.
Eine solche Funktion heißt Komponente. Kurz gesagt: Der Bildschirm ist eine Funktion der Daten, und die Komponente ist diese Funktion. React und Vue machen das auf unterschiedliche Weise. Die Idee ist dieselbe, und die Wahl zwischen den beiden ist Sache des Teams, nicht einer „besseren“ Bibliothek.
Sieh es dir an
Die erste Komponente
In React ist eine Komponente eine Funktion, die JSX zurückgibt: eine Schreibweise, die wie HTML aussieht, aber in JavaScript lebt.
const product = { name: 'Frischmilch', price: 1.6, stock: 35 };
export default function App() { return ( <main> <h1>{product.name}</h1> <p>Preis: {money(product.price)}</p> </main> );}function App()ist eine gewöhnliche Funktion. Der Name beginnt mit einem Großbuchstaben: So unterscheidet React eine Komponente von einem HTML-Tag.return ( … )gibt die Beschreibung zurück. Die Klammern sind nur dafür da, dass JSX in einer neuen Zeile beginnen darf.{product.name}ist ein JavaScript-Ausdruck in geschweiften Klammern. Sein Ergebnis wird zu Text auf der Seite.export defaulterlaubt einer anderen Datei, die Komponente zu importieren. Die Sandbox setztAppselbst auf die Seite. In einem echten Projekt macht das eine Zeile inmain.jsx:createRoot(...).render(<App />).
Konsole
Ändere den Preis oder den Lagerbestand im Objekt product und klick auf „Ausführen“. Die Beschreibung des Bildschirms fasst du nicht an: Der Bildschirm folgt den Daten.
Die Idee
Die Regeln von JSX
JSX sieht aus wie HTML, ist aber kein HTML: Bevor es läuft, wird jedes Tag zu einem Funktionsaufruf in JavaScript. Deshalb hat es eigene Regeln.
- Eine Wurzel.
returngibt ein Element zurück. Sollen mehrere nebeneinanderstehen, packst du sie in ein Fragment<>…</>: Es erzeugt kein Element auf der Seite. classNamestattclass,htmlForstattfor.classundforsind JavaScript-Wörter. Mitclassfunktioniert die Seite noch, aber React schreibt einen Fehler in die Konsole: „Invalid DOM propertyclass. Did you meanclassName?“.{Ausdruck}in Text und in Attributen:<img src={product.image} alt={product.name} />. Keine Anführungszeichen um die Klammern:alt="{product.name}"ist der Text{product.name}, Buchstabe für Buchstabe.- Jedes Tag wird geschlossen, auch die ohne Inhalt:
<input />,<img />,<br />. - Ereignisse schreibst du in camelCase und gibst ihnen eine Funktion:
onClick={add}. NichtonClick={add()}: Das ruftaddsofort auf, während die Komponente gezeichnet wird, und gibtonClickdas Ergebnis.
Probier es aus
Finde den Fehler
Ein Klick auf „Hinzufügen“ schreibt nichts in die Konsole.
Wähle die Zeile mit dem Fehler.
onclick="add()" ist HTML-Schreibweise. React nimmt Ereignisse nur als onClick mit einer Funktion: onClick={add}. Es überspringt dieses Attribut und schreibt einen Fehler in die Konsole: „Invalid event handler property onclick. Did you mean onClick?“.Probier es aus
Ergänze
Der Screenreader soll den Produktnamen als Beschreibung des Bildes vorlesen.
<img src={product.image} alt= />alt={product.name}: Der Ausdruck steht in geschweiften Klammern, ohne Anführungszeichen.Die Idee
Komponenten aus Komponenten
Die Shop-Seite hat Teile: den oberen Bereich mit dem Menü, die Produktkarte, den Warenkorb. Jeder Teil wird zu einer Komponente in einer eigenen Datei, und eine größere Komponente nutzt sie als Tags. Dann ist App keine lange Funktion, sondern ein paar Zeilen, die sagen, was auf der Seite steht.
export default function ShopHeader() { return ( <header> <a href="/">Der Shop</a> <nav>…</nav> </header> );}import ShopHeader from './ShopHeader';import ProductCard from './ProductCard';
export default function App() { return ( <> <ShopHeader /> <main> <ProductCard /> </main> </> );}Der Komponentenname beginnt mit einem Großbuchstaben, und das ist keine Geschmacksfrage. Für React ist <ShopHeader /> eine Komponente, und <shopHeader /> ist ein HTML-Tag <shopheader>, das der Browser nicht kennt. Dann steht ein Fehler in der Konsole, und der obere Bereich fehlt. Das Fragment <>…</> ist da, weil ShopHeader und <main> nebeneinanderstehen.
Probier es aus
Setz die Seite zusammen
ShopHeader und ProductCard sind in ihren Dateien fertig. Setz App zusammen: ShopHeader oben und die Produktkarte in main, unter der Überschrift „Milchprodukte“.
Konsole
Lösung
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Milchprodukte</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">Der Shop</a>
<nav>
<a href="/bread">Brot</a>
<a href="/milk">Milch</a>
<a href="/cart">Warenkorb</a>
</nav>
</header>
);
}
ProductCard.jsxconst product = { name: 'Frischmilch', price: 1.6, stock: 35 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard() {
return (
<article>
<h2>{product.name}</h2>
<p>Preis: {money(product.price)}</p>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
App importiert die beiden Komponenten mit import und nutzt sie als Tags: <ShopHeader /> vor <main>, <ProductCard /> in <main>. Sie stehen nebeneinander, deshalb sind sie in einem Fragment <>…</>. Die Prüfung sucht den oberen Bereich über die Rolle: Ein <header> in <main> ist nicht mehr der Kopf der Seite, und ein kleingeschriebenes <shopHeader /> hat gar kein <header>.
Die Idee
Nur der Unterschied
Hier ist der Warenkorb aus dem Problem als Komponente.
useState('2') ist das Gedächtnis der Komponente: ein Wert, den React zwischen zwei Zeichnungen behält, und eine Funktion (setMilk), die ihn ändert und React sagt, die Komponente neu zu zeichnen. Das ist das Thema des Moduls „Zustand“. Hier ist es nur dafür da, dass sich etwas ändern kann.
Konsole
Mach dasselbe wie im Problem: Klick ins Milchfeld, lösch die 2 und tipp 15. Das Feld behält den Fokus und das Getippte, und die Summe wird 25,20 €. Die Komponente beschreibt bei jedem Tastendruck den ganzen Bildschirm, wie render(). Der Unterschied ist, dass die Seite nicht neu gebaut wird. Die Bibliothek sieht, dass das Feld in der neuen Beschreibung an derselben Stelle mit demselben Tag steht, und ändert nur seinen Wert und den Text der Summe.
Sieh es dir an
Sieh dir den Vergleich an
import { useState } from 'react';const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function App() { const [milk, setMilk] = useState('2'); const [bread, setBread] = useState('1'); const total = Number(milk) * 1.6 + Number(bread) * 1.2;
return ( <main> <h1>Der Warenkorb</h1> <label> Frischmilch <input type="number" value={milk} onChange={(e) => setMilk(e.target.value)} /> </label> <label> Weißbrot <input type="number" value={bread} onChange={(e) => setBread(e.target.value)} /> </label> <p>Gesamt: {money(total)}</p> </main> );}Schritt für Schritt, in Worten
- Der Kunde hat die 2 gelöscht und 1 getippt: Die Daten sagen milk = "1". In der Mitte steht die letzte Beschreibung, die die Komponente zurückgegeben hat; rechts steht das DOM, mit dem Fokus im Milchfeld.
- Der Kunde tippt 5. onChange ruft setMilk("15") auf: Die Daten ändern sich, und React merkt sich, dass App neu gezeichnet werden muss.
- React ruft die Funktion App noch einmal auf. Sie fasst die Seite nicht an, sondern gibt eine neue Beschreibung zurück: einen Baum aus einfachen Objekten. Ihn zu bauen ist billig.
- Die Bibliothek vergleicht die neue Beschreibung mit der vorigen. Dieselben Tags in derselben Reihenfolge und zwei Unterschiede: der Wert des Feldes und der Text der Summe.
- Nur die beiden Unterschiede erreichen das DOM. Das <input> ist dasselbe Element wie vorher, deshalb bleiben Fokus und Cursor darin.
- Du beschreibst den ganzen Bildschirm für die gegebenen Daten, wie bei render(). Die Bibliothek findet, was sich geändert hat, und fasst nur das an. Beide Bibliotheken tun das, jede auf ihre Weise.
Die Beschreibung ist billig: Es ist ein Baum aus einfachen Objekten, und die Komponente macht ihn jedes Mal neu. Das DOM ist der teure Teil, deshalb fasst die Bibliothek dort nur die Unterschiede an. Du schreibst wie bei render(): den ganzen Bildschirm aus den Daten. Der Preis von render() (verlorener Fokus, tausende neue Elemente) fällt nicht an.
Genauer, für Neugierige
Virtuelles DOM. Die Beschreibung heißt oft virtuelles DOM, und ihr Vergleich mit der alten heißt Abgleich (Reconciliation in React, Patching in Vue). Ein Knoten der Beschreibung ist ein Objekt wie { type: 'h1', props: { children: 'Der Warenkorb' } }.
Die beiden Wege. JSX wird zu Aufrufen wie jsx('h1', { children: product.name }) kompiliert. Wenn sich das Gedächtnis einer Komponente ändert, ruft React ihre Funktion noch einmal auf, dazu die Funktionen der Komponenten darin, und vergleicht dann. In beiden Fällen entstehen am Ende eine neue Beschreibung, ein Vergleich und ein paar Änderungen im DOM.
Text ist Text. {product.name} setzt Text ein, wie textContent. Ein Name wie <img src=x onerror=alert(1)> erscheint als Zeichen und wird nicht zu einem Tag. HTML aus einem String kommt nur mit dangerouslySetInnerHTML hinein, und der Name mahnt zur Vorsicht.
Klassenkomponenten. In älterem Code ist eine Komponente eine Klasse: class Cart extends React.Component { render() { … } }. Sie funktioniert noch, aber neuer Code nutzt Funktionen und Hooks (useState ist ein Hook).
StrictMode. Ein neues Vite-Projekt packt App in <StrictMode>. Im Entwicklungsmodus ruft es jede Komponente zweimal auf, um Code ans Licht zu bringen, der etwas außerhalb von sich anfasst. Deshalb kann ein console.log im Rumpf einer Komponente zweimal erscheinen. Die Sandbox zeichnet App ohne StrictMode.
Übung
Die Produktkarte
In ProductCard steht das Objekt product (Weißkäse). Bau die Karte: den Namen als Überschrift der Ebene zwei, „Preis: 7,90 €“ (mit money), „Auf Lager: 12 Stk.“ und einen Button „Hinzufügen“, dessen Name für den Screenreader „Hinzufügen Weißkäse“ ist. Ein Klick auf den Button schreibt „Hinzugefügt: Weißkäse“ in die Konsole (die Funktion add ist fertig). Setz dann App zusammen: ShopHeader oben und die Karte in main, unter der Überschrift „Milchprodukte“.
Konsole
Lösung
ProductCard.jsxconst product = { name: 'Weißkäse', price: 7.9, stock: 12 };
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard() {
function add() {
console.log(`Hinzugefügt: ${product.name}`);
}
return (
<article>
<h2>{product.name}</h2>
<p>Preis: {money(product.price)}</p>
<p>Auf Lager: {product.stock} Stk.</p>
<button type="button" aria-label={`Hinzufügen ${product.name}`} onClick={add}>
Hinzufügen
</button>
</article>
);
}
App.jsximport ShopHeader from './ShopHeader';
import ProductCard from './ProductCard';
export default function App() {
return (
<>
<ShopHeader />
<main>
<h1>Milchprodukte</h1>
<ProductCard />
</main>
</>
);
}
ShopHeader.jsxexport default function ShopHeader() {
return (
<header>
<a href="/">Der Shop</a>
<nav>
<a href="/bread">Brot</a>
<a href="/milk">Milch</a>
<a href="/cart">Warenkorb</a>
</nav>
</header>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
nav a { margin-left: 12px; }
main { padding: 0 16px; }
h1 { font-size: 22px; }
label { display: block; margin: 8px 0; }
input { width: 4em; font: inherit; margin-left: 8px; }
article { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 12px 12px; max-width: 320px; }
button { font: inherit; }
Alles aus den Daten steht in {…}: {product.name}, {money(product.price)}, {product.stock}. Der Name für den Screenreader ist ein Ausdruck: aria-label={`Hinzufügen ${product.name}`}. Der Button bekommt die Funktion: onClick={add}. Mit onClick={add()} erscheint „Hinzugefügt: Weißkäse“ schon beim Laden in der Konsole, ohne Klick, und genau danach sucht die Prüfung.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Barrierefreiheit · Das DOM: die Seite aus JavaScript · Ereignisse · Eine Liste aus Daten
- Geht weiter in: Props: Daten von oben · Listen und Keys · Bedingungen: leer, Fehler, lädt · Zustand
- Aufgaben zum Thema: Komponenten