Zum Inhalt
wannacode

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.

HTML, CSS, JS

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 default erlaubt einer anderen Datei, die Komponente zu importieren. Die Sandbox setzt App selbst auf die Seite. In einem echten Projekt macht das eine Zeile in main.jsx: createRoot(...).render(<App />).
React

Ä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. return gibt ein Element zurück. Sollen mehrere nebeneinanderstehen, packst du sie in ein Fragment <>…</>: Es erzeugt kein Element auf der Seite.
  • className statt class, htmlFor statt for. class und for sind JavaScript-Wörter. Mit class funktioniert die Seite noch, aber React schreibt einen Fehler in die Konsole: „Invalid DOM property class. Did you mean className?“.
  • {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}. Nicht onClick={add()}: Das ruft add sofort auf, während die Komponente gezeichnet wird, und gibt onClick das Ergebnis.

Probier es aus

Finde den Fehler

Ein Klick auf „Hinzufügen“ schreibt nichts in die Konsole.

Wähle die Zeile mit dem Fehler.

Probier es aus

Ergänze

Der Screenreader soll den Produktnamen als Beschreibung des Bildes vorlesen.

<img src={product.image} alt= />

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.

ShopHeader.jsx
export default function ShopHeader() {
return (
<header>
<a href="/">Der Shop</a>
<nav>…</nav>
</header>
);
}
App.jsx
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“.

React

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.

React

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
  1. 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.
  2. Der Kunde tippt 5. onChange ruft setMilk("15") auf: Die Daten ändern sich, und React merkt sich, dass App neu gezeichnet werden muss.
  3. 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.
  4. 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.
  5. Nur die beiden Unterschiede erreichen das DOM. Das <input> ist dasselbe Element wie vorher, deshalb bleiben Fokus und Cursor darin.
  6. 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“.

React

Test

Teste dich

1. Eine Komponente wird ein zweites Mal mit denselben Daten gezeichnet. Was zeigt sie?
2. Welche Zeile ist gültiges JSX?
3. Warum bleibt der Fokus in der Komponente im Feld, während render() aus dem vorigen Modul ihn verliert?
4. In der Datei steht function productCard(), und App nutzt <productCard />. Was passiert?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·