Bedingungen: leer, Fehler, lädt
Kurz gesagt: Bedingtes Rendern zeigt nur den Teil des Markups, der zu den Daten passt (lädt, Fehler, leer oder die Liste); in React mit && und dem ternären Operator.
Problem
Lädt, Fehler und die Liste gleichzeitig
Der Katalog ist geladen. Unter der Überschrift steht aber „Wird geladen…“, dann „Die Produkte wurden nicht geladen.“, dann „In dieser Kategorie gibt es keine Produkte.“, und erst danach kommen die Produkte.
Konsole
Die Komponente Catalog bekommt status und die Produkte als Props. Aber sie schaut nicht darauf: Sie zeichnet immer alle vier Teile. Der Käufer weiß nicht, was er glauben soll, und ein Screenreader liest sowohl den Fehler als auch die Produkte vor.
Die Idee
Der Teil, der zu den Daten passt
Der Katalog hat vier Zustände: lädt, Fehler, leer und bereit. Nur der Teil für den aktuellen soll auf dem Bildschirm sein. Das ist bedingtes Rendern: Die Komponente entscheidet anhand der Daten, was sie zurückgibt.
JSX hat drei Wege, je nachdem, wie viel sich ändert:
// Eine ganz andere Ansicht: if und ein frühes returnif (status === 'loading') return <p role="status">Wird geladen…</p>;
// Eins von zwei: ? :<p>{product.stock === 0 ? 'Ausverkauft' : 'Verfügbar'}</p>
// Etwas oder nichts: &&{status === 'error' && <p role="alert">Die Produkte wurden nicht geladen.</p>}if ist gewöhnliches JavaScript: Die Komponente ist eine Funktion und kann verschiedenes JSX zurückgeben. ? : und && sind Ausdrücke, deshalb können sie in JSX stehen, in {}.
Die beiden Bibliotheken lösen dasselbe auf ihre Weise: React mit JavaScript um das Markup herum, Vue mit Attributen darin. Das Ergebnis ist dasselbe: Das Element einer falschen Bedingung ist nicht im DOM. Es ist nicht versteckt, es existiert nicht.
Hier wird status als Prop übergeben. Wie der Katalog ihn beim Laden aus der API selbst ändert, steht im Modul „Effekte, Daten und Formulare“.
Sieh es dir an
Sieh, welcher Zweig hineinkommt
Ein Wert geht in die Komponente, und nur ein Zweig kommt ins DOM. Achte bei jeder Änderung von status auf die Spalte „DOM“.
function Catalog({ status, products }) { if (status === 'loading') return <p>Wird geladen…</p>; if (status === 'error') return <p>Fehler</p>; if (status === 'empty') return <p>Keine Produkte</p>; return <ul>…</ul>;}
// im DOM, aber versteckt<p hidden={status !== 'loading'}>Wird geladen…</p>Schritt für Schritt, in Worten
- status kommt als Prop in die Komponente. Er ist 'loading': Das erste if passt, und die Komponente gibt nur <p>Wird geladen…</p> zurück. Nur das kommt ins DOM.
- status wird 'error'. Die Bibliothek zeichnet neu: Jetzt passt der zweite Zweig. <p>Wird geladen…</p> verlässt das DOM, und die Fehlermeldung kommt hinein.
- 'empty': nur die Meldung, dass die Kategorie leer ist.
- 'ready': Keines der if passt, und die Komponente kommt beim letzten return an: der Liste. Im DOM ist immer ein Zweig.
- Der andere Weg: Das Element wird immer gezeichnet, und das Attribut hidden entscheidet, ob man es sieht. Bei loading ist hidden false, und der Text ist sichtbar.
- status wird 'ready'. Das Element bleibt mit allem, was darin ist, im DOM und bekommt nur display: none. Niemand sieht es, und ein Screenreader überspringt es.
- Eine Bedingung mit if, ? : oder && entfernt das Element aus dem DOM. hidden versteckt es nur. Entfernen ist der Normalfall; Verstecken ist für etwas, das oft umgeschaltet wird oder das Eingetippte behalten muss.
Der erste Teil ist bedingtes Rendern: Bei einem neuen Wert verlässt das alte Element das DOM, und das neue kommt hinein. Der zweite Teil ist Verstecken: Das Element ist die ganze Zeit im DOM und wird nur gezeigt oder versteckt. Darum geht es am Ende der Lektion.
Probier es aus
Die vier Zustände
Catalog zeichnet alles auf einmal. Baue es so um, dass jeder Abschnitt nur den Teil seines Zustands hat: „Wird geladen…“ bei lädt, den Fehler bei Fehler, „In dieser Kategorie gibt es keine Produkte.“ bei leer und die Liste bei bereit. Die anderen Teile dürfen nicht auf der Seite sein.
Konsole
Lösung
Catalog.jsximport ProductCard from './ProductCard';
export default function Catalog({ status, products }) {
if (status === 'loading') return <p role="status">Wird geladen…</p>;
if (status === 'error') return <p role="alert">Die Produkte wurden nicht geladen.</p>;
if (status === 'empty') return <p>In dieser Kategorie gibt es keine Produkte.</p>;
return (
<ul>
{products.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
);
}
App.jsximport Catalog from './Catalog';
const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, stock: 0 },
{ id: 2, name: 'Frischmilch', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<section aria-label="Lädt">
<h2>Lädt</h2>
<Catalog status="loading" products={[]} />
</section>
<section aria-label="Fehler">
<h2>Fehler</h2>
<Catalog status="error" products={[]} />
</section>
<section aria-label="Leere Kategorie">
<h2>Leere Kategorie</h2>
<Catalog status="empty" products={[]} />
</section>
<section aria-label="Bereit">
<h2>Bereit</h2>
<Catalog status="ready" products={PRODUCTS} />
</section>
</main>
);
}
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
<button type="button" aria-label={`Hinzufügen ${product.name}`}>Hinzufügen</button>
</article>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
if mit einem frühen return für die drei Meldungen, und die Liste ist das letzte return. Die Prüfung sucht in jedem Abschnitt sowohl die Meldungen als auch die Liste. Hat die Liste keine Bedingung, ist sie auch im Abschnitt „Lädt“, obwohl sie leer ist: Ein <ul> ohne Zeilen ist für einen Screenreader trotzdem eine Liste.Probier es aus
Ergänze die Lücke
„Wird geladen…“ soll nur erscheinen, solange status loading ist.
{status === 'loading' <p role="status">Wird geladen…</p>}&&: Ist die linke Seite falsch, ist der Ausdruck false, und React zeichnet nichts. Ist sie wahr, ist der Ausdruck das Element rechts.
Die Idee
Ausverkauft: ein Abzeichen und ein deaktivierter Button
Eine Bedingung gibt es nicht nur für ganze Teile. In der Produktkarte ändert sie einen Text und ein Attribut:
{product.stock === 0 ? ( <p className="badge">Ausverkauft</p>) : ( <p>Nur noch {product.stock} übrig</p>)}<button type="button" disabled={product.stock === 0}>Hinzufügen</button>disabled={true} setzt das Attribut disabled, und disabled={false} entfernt es.
Ein deaktivierter Button lässt sich nicht drücken, bekommt mit Tab keinen Fokus, und ein Screenreader sagt, dass er nicht verfügbar ist. Das Abzeichen sagt, warum. Ein Button, den man drücken kann und der dann „nicht auf Lager“ sagt, ist schlechter: Der Käufer erfährt es erst nach dem Klick.
Probier es aus
Ausverkauft
Sorge dafür, dass ProductCard „Ausverkauft“ zeigt und den Button „Hinzufügen“ deaktiviert, wenn stock 0 ist. Sonst zeigt sie „Nur noch 5 übrig“, und der Button funktioniert. Der Käse ist ausverkauft.
Konsole
Lösung
ProductCard.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function ProductCard({ product }) {
return (
<article>
<h3>{product.name}</h3>
<p>{money(product.price)}</p>
{product.stock === 0 ? (
<p className="badge">Ausverkauft</p>
) : (
<p>Nur noch {product.stock} übrig</p>
)}
<button type="button" aria-label={`Hinzufügen ${product.name}`} disabled={product.stock === 0}>Hinzufügen</button>
</article>
);
}
App.jsximport ProductCard from './ProductCard';
const PRODUCTS = [
{ id: 1, name: 'Weißbrot', price: 1.2, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, stock: 0 },
{ id: 2, name: 'Frischmilch', price: 1.6, stock: 2 },
];
export default function App() {
return (
<main>
<h1>Der Katalog</h1>
<ul>
{PRODUCTS.map((p) => (
<li key={p.id}>
<ProductCard product={p} />
</li>
))}
</ul>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
? : wählt zwischen dem Abzeichen und „Nur noch … übrig“, und disabled={product.stock === 0} deaktiviert den Button. Die Prüfung sucht außerdem eine einzelne 0 auf der Seite: {product.stock && <p>…</p>} zeichnet beim Käse eine 0. Davon handelt die nächste Karte.Die Idee
Die Null auf dem Bildschirm
&& gibt nicht true oder false zurück. Es gibt die linke Seite zurück, wenn sie falsy ist, und die rechte Seite, wenn sie truthy ist. React zeichnet Zahlen, auch die 0, und zeichnet false, null und undefined nicht. Also:
{items.length && <p>Im Warenkorb: {items.length}</p>} // leerer Warenkorb: 0{items.length > 0 && <p>Im Warenkorb: {items.length}</p>} // leerer Warenkorb: nichtsSetz vor && einen Vergleich, der true oder false ergibt: > 0, === 0, !== null. Ein anderer Weg ist ? : mit null für „nichts“.
Probier es aus
Finde den Fehler
Bei einem leeren Warenkorb steht in der oberen Ecke „0“.
Wähle die Zeile mit dem Fehler.
Zeile 4: items.length && … ist bei einem leeren Warenkorb 0, und React zeichnet die 0. Es muss items.length > 0 && … heißen.
Die Idee
Entfernt oder versteckt
Manchmal muss ein Element im DOM bleiben und nur nicht zu sehen sein. Hier haben beide Panels ein Feld für eine Notiz. Öffne beide, tipp etwas ein, schließ sie und öffne sie wieder.
Konsole
{removedOpen && …}: Geschlossen ist es weg, und geöffnet ist es neu, mit leerem Feld. Im zweiten nutzt es das Attribut hidden={!hiddenOpen}: Es bleibt mit display: none im DOM und behält das Eingetippte.Entfernen ist der Normalfall: weniger Elemente auf der Seite, nichts Verstecktes für einen Screenreader. Verstecken ist für Teile, die oft geöffnet und geschlossen werden oder behalten müssen, was darin eingegeben wurde.
Genauer, für Neugierige
hidden und CSS. hidden wirkt über display: none aus den Stilen des Browsers. Gibt eine CSS-Regel dem Element display: flex, gewinnt die Regel, und das Element ist trotzdem sichtbar. Dann versteckst du es mit einer Klasse oder mit style={{ display: 'none' }}.
Gleiche Stelle, gleiches Element. Bei {isSearch ? <input placeholder="Suchen" /> : <input placeholder="Code" />} ist jeder Zweig ein <input> an derselben Stelle. React behält das Element und ändert nur placeholder, und das Eingetippte bleibt. Brauchst du zwei verschiedene Elemente, gib ihnen verschiedene keys.
return null. Eine Komponente, die nichts zeichnen soll, gibt null zurück.
Klassenkomponenten in altem Code machen dasselbe in der Methode render().
aria-hidden ist kein Verstecken. aria-hidden="true" versteckt das Element nur vor einem Screenreader, und es bleibt auf dem Bildschirm. Vor allen zu verstecken geht mit display: none: hidden, v-show oder Entfernen.
Übung
Der Warenkorb
App zeigt drei Warenkörbe: leer, mit Produkten und mit einem ausverkauften Produkt. Baue Cart so, dass der leere „Der Warenkorb ist leer.“ sagt, ohne Liste und ohne Summe, und der Button „Bestellen“ deaktiviert ist. Der volle zeigt die Zeilen, „Gesamt: 4,00 €“ und einen funktionierenden Button. Das ausverkaufte Produkt hat ein Abzeichen „Ausverkauft“, über dem Button steht „Entferne die ausverkauften Produkte, um zu bestellen.“ (Rolle alert), und der Button ist deaktiviert.
Konsole
Lösung
Cart.jsxconst money = (x) => x.toFixed(2).replace('.', ',') + ' €';
export default function Cart({ items }) {
const total = items.reduce((sum, p) => sum + p.price * p.qty, 0);
const soldOut = items.some((p) => p.stock === 0);
return (
<div>
{items.length === 0 ? (
<p>Der Warenkorb ist leer.</p>
) : (
<>
<ul>
{items.map((p) => (
<li key={p.id}>
{p.name} × {p.qty}
{p.stock === 0 && <span className="badge">Ausverkauft</span>}
</li>
))}
</ul>
<p>Gesamt: {money(total)}</p>
</>
)}
{soldOut && <p role="alert">Entferne die ausverkauften Produkte, um zu bestellen.</p>}
<button type="button" disabled={items.length === 0 || soldOut}>Bestellen</button>
</div>
);
}
App.jsximport Cart from './Cart';
export default function App() {
return (
<main>
<section aria-label="Leer">
<h2>Leer</h2>
<Cart items={[]} />
</section>
<section aria-label="Mit Produkten">
<h2>Mit Produkten</h2>
<Cart items={[
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 2, stock: 5 },
{ id: 2, name: 'Frischmilch', price: 1.6, qty: 1, stock: 2 },
]} />
</section>
<section aria-label="Mit einem ausverkauften Produkt">
<h2>Mit einem ausverkauften Produkt</h2>
<Cart items={[
{ id: 1, name: 'Weißbrot', price: 1.2, qty: 1, stock: 5 },
{ id: 3, name: 'Weißkäse', price: 7.9, qty: 1, stock: 0 },
]} />
</section>
</main>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 16px; }
h1 { font-size: 22px; margin: 0 0 12px; }
section { border: 1px dashed #bbb; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
section > h2 { font-size: 13px; color: #666; margin: 0 0 6px; text-transform: uppercase; }
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }
article { display: flex; align-items: center; gap: 12px; border: 1px solid #ddd; border-radius: 8px; padding: 6px 12px; }
article h3 { font-size: 16px; margin: 0; flex: 1; }
p { margin: 4px 0; }
[role="alert"] { color: #b3261e; }
.badge { background: #fde2dd; color: #8c1d18; border-radius: 4px; padding: 0 6px; font-size: 13px; }
button { font-size: 15px; padding: 4px 12px; }
textarea { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; }
items.length === 0 ? … : … wählt zwischen der Meldung und der Liste mit der Summe (die beiden stehen in <>…</>). soldOut && <p role="alert">…</p> zeigt die Warnung, und disabled={items.length === 0 || soldOut} deaktiviert den Button in beiden Fällen. Mit items.length && … zeigt der leere Warenkorb eine 0, und die Prüfung sucht genau das.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Daten aus einer API · Die erste Komponente · Props: Daten von oben · Listen und Keys
- Geht weiter in: Objekte und Arrays aktualisieren · Abgeleiteter Zustand · Gemeinsamer Zustand: nach oben · Effekte
- Aufgaben zum Thema: Komponenten