Zum Inhalt springen
aviral gupta

// B2.6 · ca. 40 Min. · Einstieg

Reinheit und der UI-Baum

In diesem Build halten Sie Komponenten rein, sehen, was Strict Mode aufdeckt, lesen Ihre App als zwei Bäume und bauen eine Profilgalerie über drei Dateien.

Lektion 6 von 6 in B2 Die Oberfläche beschreiben

Ende des Moduls

Danach können Sie

  • Reine Komponenten schreiben, die außen nichts ändern und für dieselben Props dasselbe JSX liefern
  • Erklären, warum Strict Mode unreine Ausgaben verdoppelt, und Nebenwirkungen in Event-Handlern halten
  • Den Render-Baum und den Modul-Abhängigkeitsbaum einer Galerie aus mehreren Dateien lesen
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B2.5: filter, map und Keys. Welches HTML setzt Todo in #root?

    const tasks = [
      { id: 1, title: 'Shop', done: true },
      { id: 2, title: 'Cook', done: false }
    ];
    
    function Todo() {
      return (
        <ul>
          {tasks.filter(t => !t.done).map(t => <li key={t.id}>{t.title}</li>)}
        </ul>
      );
    }
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Der Runner des Kurses rendert wie Vites Vorlage in <StrictMode>. Welche Nummern zeigen die drei Tickets?

    let ticket = 0;
    
    function Ticket() {
      ticket = ticket + 1;
      return <li>Ticket #{ticket}</li>;
    }
    
    function Queue() {
      return (
        <ul>
          <Ticket />
          <Ticket />
          <Ticket />
        </ul>
      );
    }
  3. Üben · Aufgabe 3 von 7

    Die Elternkomponente übergibt jetzt number={1}, number={2} und number={3}. Ergänzen Sie das Prop, das Ticket rein macht.

    function Ticket({ ____ }) {
      return <li>Ticket #{number}</li>;
    }
    function Ticket({ }) {
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Stück Code zu, was es ist.

  5. Üben · Aufgabe 5 von 7

    Lowest zeigt die kleinste Punktzahl. Was enthält das Array scores, nachdem es gerendert hat?

    const scores = [3, 1, 2];
    
    function Lowest() {
      const sorted = scores.sort();
      return <p>Lowest: {sorted[0]}</p>;
    }
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Stars pusht beim Rendern in ein Array, und Strict Mode ruft es zweimal auf. Welchen Text zeigt Rating?

    function Stars({ count }) {
      const row = [];
      for (let i = 0; i < count; i++) {
        row.push('★');
      }
      return <p>{row.join('')}</p>;
    }
    
    function Rating() {
      return <Stars count={3} />;
    }
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: Bestellungen in einem Event-Handler. Schreiben Sie in main.jsx eine Komponente Order mit einem Prop drink und einem State cups. Ihr Button zeigt das Getränk und wie viele Tassen bestellt wurden. Ein Klick fügt eine Tasse hinzu und gibt mit console.log "ordered Tea" (oder das jeweilige Getränk) aus. Legen Sie beide Änderungen in den Klick-Handler, keine in den Komponentenkörper. Rendern Sie zwei Orders. Führen Sie es aus und klicken Sie einmal: Die Zeile erscheint einmal, obwohl Strict Mode Order zweimal rendert.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Rein und unrein im Strict Mode

App rendert zwei Impure-Komponenten, zwei Pure und Seats. Impure zählt bei jedem Aufruf visits hoch, eine Variable außerhalb. Pure liest nur sein Prop. Seats, in einer eigenen Datei, pusht in ein Array, das es gerade angelegt hat. Der Runner rendert in <StrictMode>, das jede Komponente zweimal aufruft. Render-Baum: App ist die Wurzel, die anderen Komponenten sind seine Blätter. Modul-Abhängigkeitsbaum: main.jsx importiert Seats.jsx.

main.jsx

import Seats from './Seats.jsx';

let visits = 0;

function Impure() {
  visits = visits + 1;
  return <p>Impure: visit #{visits}</p>;
}

function Pure({ visit }) {
  return <p>Pure: visit #{visit}</p>;
}

export default function App() {
  return (
    <section>
      <Impure />
      <Impure />
      <Pure visit={1} />
      <Pure visit={2} />
      <Seats count={3} />
    </section>
  );
}

Seats.jsx

export default function Seats({ count }) {
  const seats = [];
  for (let i = 1; i <= count; i++) {
    seats.push(<li key={i}>Seat {i}</li>);
  }
  return <ul>{seats}</ul>;
}

Ausführen mit

npm run dev

Ausgabe

<!-- #root -->
<div id="root"><section><p>Impure: visit #2</p><p>Impure: visit #4</p><p>Pure: visit #1</p><p>Pure: visit #2</p><ul><li>Seat 1</li><li>Seat 2</li><li>Seat 3</li></ul></section></div>
  • Impure wurde viermal aufgerufen, zweimal pro Komponente, deshalb zeigen seine zwei Exemplare #2 und #4.
  • Pure zeigt genau sein Prop, egal wie oft es aufgerufen wird.
  • Seats baut bei jedem Aufruf drei Sitze, weil sein Array jedes Mal neu ist: lokale Mutation.
  • In Produktion tut Strict Mode nichts, und Impure zeigte #1 und #2: Der Fehler bliebe verborgen.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Übungen

Übung 1 von 2

Eine laufende Summe ohne Variable außerhalb

Jede Cup soll ihre Größe zeigen und wie viel bisher eingeschenkt wurde: 200, dann 450, dann 750 ml. Führen Sie es aus: Strict Mode macht die Summen falsch, weil Cup zu poured addiert, einer Variablen außerhalb. Machen Sie Cup rein: Es bekommt ein zweites Prop, soFar, und zeigt nur seine Props. Rechnen Sie die laufende Summe in TeaRound mit einer lokalen Variablen aus, während Sie die Tassen mappen. Behalten Sie die Namen Cup und TeaRound und ihre Exporte.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Cup wird zu function Cup({ size, soFar }) und rendert {soFar} statt poured.

  2. Hinweis 2

    Deklarieren Sie in TeaRound vor dem map let poured = 0;. Es entsteht in diesem Rendern, sie zu ändern ist also lokale Mutation.

  3. Hinweis 3

    Nehmen Sie im map einen Block-Body: poured = poured + cup.size; dann return <Cup key={cup.id} size={cup.size} soFar={poured} />;.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

const cups = [
  { id: 'c1', size: 200 },
  { id: 'c2', size: 250 },
  { id: 'c3', size: 300 }
];

export function Cup({ size, soFar }) {
  return <li>{size} ml, {soFar} ml so far</li>;
}

export default function TeaRound() {
  let poured = 0;
  const items = cups.map(cup => {
    poured = poured + cup.size;
    return <Cup key={cup.id} size={cup.size} soFar={poured} />;
  });
  return <ul>{items}</ul>;
}
Auf dem eigenen Computer ausführen

Installieren Sie React 19.3 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.jsx

const cups = [
  { id: 'c1', size: 200 },
  { id: 'c2', size: 250 },
  { id: 'c3', size: 300 }
];

let poured = 0;

// TODO: make Cup pure: it may only use its props.
// Work out the running total in TeaRound instead.
export function Cup({ size }) {
  poured = poured + size;
  return <li>{size} ml, {poured} ml so far</li>;
}

export default function TeaRound() {
  return (
    <ul>
      {cups.map(cup => <Cup key={cup.id} size={cup.size} />)}
    </ul>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import TeaRound, {Cup} from './main.jsx';

test('Die Tassen zeigen laufende Summen von 200, 450 und 750 ml', () => {
  render(<TeaRound />);
  const items = screen.getAllByRole('listitem').map((li) => li.textContent);
  assert.deepEqual(items, ['200 ml, 200 ml so far', '250 ml, 450 ml so far', '300 ml, 750 ml so far'], 'die Liste lautet ' + JSON.stringify(items));
});

test('Cup zeigt, was seine Props sagen', () => {
  render(<ul><Cup size={100} soFar={300} /></ul>);
  const item = screen.getByRole('listitem').textContent;
  assert.equal(item, '100 ml, 300 ml so far', 'die Tasse lautet ' + JSON.stringify(item));
});

test('Zweimal TeaRound rendern ergibt dasselbe HTML', () => {
  const first = render(<TeaRound />).container.innerHTML;
  const second = render(<TeaRound />).container.innerHTML;
  assert.equal(second, first, 'das zweite Rendern weicht vom ersten ab');
});

Legen Sie main.jsx in einem Vite-React-Projekt (npm create vite@latest, Vorlage react) als App.jsx in src/ ab und starten Sie es mit npm run dev. React rendert es dort wie hier in <StrictMode>.

Programm ausführen:

npm run dev

Für die Prüfungen gibt es noch keinen Befehl auf Ihrem Computer. Sie stehen in main.test.jsx, geschrieben mit node:test und React Testing Library.

Übung 2 von 2

Build: eine Profilgalerie

Die Galerie verteilt sich auf drei Dateien: data.js enthält die Personen, Profile.jsx die Komponenten Profile und Avatar, main.jsx die Gallery. Gallery ist unrein: Sie sortiert das importierte Array an Ort und Stelle, nummeriert die Profile mit einer Variablen außerhalb der Komponente und vergibt keine Keys. Schreiben Sie Gallery neu: eine Kopie nach Namen sortieren, die Profile über den Index aus map von 1 bis 4 nummerieren und jedem Profile key={person.id} geben. Zweimal rendern muss dasselbe HTML ergeben, und data.js muss unverändert bleiben.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser React und den Compiler herunter, der JSX in JavaScript übersetzt (bis zu 2 MB), und speichert beides im Cache. Ihre Komponenten laufen in Ihrem Browser und bleiben auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    [...people] macht ein neues Array, das Sie sortieren dürfen: const sorted = [...people].sort((a, b) => a.name.localeCompare(b.name));.

  2. Hinweis 2

    map() übergibt den Index als zweites Argument: sorted.map((person, index) => …), und position={index + 1}.

  3. Hinweis 3

    Löschen Sie let position = 0; und verwenden Sie <Profile key={person.id} person={person} position={index + 1} />.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

import Profile from './Profile.jsx';
import { people } from './data.js';

export default function Gallery() {
  const sorted = [...people].sort((a, b) => a.name.localeCompare(b.name));
  return (
    <section>
      <h1>Scientist gallery</h1>
      <p>{sorted.length} profiles</p>
      {sorted.map((person, index) => (
        <Profile key={person.id} person={person} position={index + 1} />
      ))}
    </section>
  );
}
Auf dem eigenen Computer ausführen

Installieren Sie React 19.3 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.jsx

import Profile from './Profile.jsx';
import { people } from './data.js';

let position = 0;

// TODO: sort by name without changing people, number the profiles
// from 1 without the outside variable, and give each Profile a key.
export default function Gallery() {
  people.sort((a, b) => a.name.localeCompare(b.name));
  return (
    <section>
      <h1>Scientist gallery</h1>
      <p>{people.length} profiles</p>
      {people.map(person => {
        position = position + 1;
        return <Profile person={person} position={position} />;
      })}
    </section>
  );
}

main.test.jsx

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {render, screen} from '@testing-library/react';
import Gallery from './main.jsx';
import {people} from './data.js';

test('React gibt keine Warnungen aus', () => {
  const warnings = [];
  const original = console.error;
  console.error = (...args) => warnings.push(args.join(' '));
  try {
    render(<Gallery />);
  } finally {
    console.error = original;
  }
  assert.deepEqual(warnings, [], 'console.error beim Rendern');
});

test('Vier Profile, nach Namen sortiert und ab 1 nummeriert', () => {
  render(<Gallery />);
  const names = screen.getAllByRole('heading', {level: 2}).map((h) => h.textContent);
  assert.deepEqual(names, ['1. Alan Turing', '2. C. V. Raman', '3. Marie Skłodowska-Curie', '4. Rosalind Franklin'], 'die Profile lauten ' + JSON.stringify(names));
  assert.equal(screen.getByText(/profiles$/).textContent, '4 profiles', 'die Zeile mit der Anzahl');
});

test('Jedes Profil zeigt einen Avatar mit dem Namen der Person', () => {
  render(<Gallery />);
  const avatars = screen.getAllByRole('img').map((img) => img.getAttribute('aria-label'));
  assert.equal(avatars.length, 4, 'Avatare: ' + avatars.length);
  assert.ok(avatars.includes('Alan Turing'), 'kein Avatar heißt Alan Turing');
});

test('Rendern ändert das Array in data.js nicht', () => {
  render(<Gallery />);
  const ids = people.map((person) => person.id);
  assert.deepEqual(ids, ['msc', 'atu', 'rfr', 'cvr'], 'data.js enthält jetzt ' + JSON.stringify(ids));
});

test('Zweimal rendern ergibt dasselbe HTML', () => {
  const first = render(<Gallery />).container.innerHTML;
  const second = render(<Gallery />).container.innerHTML;
  assert.equal(second, first, 'das zweite Rendern weicht vom ersten ab');
});

Profile.jsx

export function Avatar({ person }) {
  const initials = person.name
    .split(' ')
    .map(part => part[0])
    .join('');
  return (
    <span className="avatar" role="img" aria-label={person.name}>
      {initials}
    </span>
  );
}

export default function Profile({ person, position }) {
  return (
    <article className="profile">
      <h2>
        {position}. {person.name}
      </h2>
      <Avatar person={person} />
      <p>
        {person.profession}, known for {person.discovery}
      </p>
    </article>
  );
}

data.js

export const people = [
  { id: 'msc', name: 'Marie Skłodowska-Curie', profession: 'physicist and chemist', discovery: 'polonium and radium' },
  { id: 'atu', name: 'Alan Turing', profession: 'mathematician', discovery: 'the Turing machine' },
  { id: 'rfr', name: 'Rosalind Franklin', profession: 'chemist', discovery: 'X-ray images of DNA' },
  { id: 'cvr', name: 'C. V. Raman', profession: 'physicist', discovery: 'Raman scattering' }
];

styles.css

.profile {
  border: 1px solid #ccc;
  border-radius: 8px;
  margin: 8px 0;
  padding: 8px 12px;
}

.avatar {
  display: inline-block;
  width: 48px;
  height: 48px;
  line-height: 48px;
  border-radius: 50%;
  background: #446;
  color: white;
  text-align: center;
}

Legen Sie main.jsx in einem Vite-React-Projekt (npm create vite@latest, Vorlage react) als App.jsx in src/ ab und starten Sie es mit npm run dev. React rendert es dort wie hier in <StrictMode>.

Programm ausführen:

npm run dev

Für die Prüfungen gibt es noch keinen Befehl auf Ihrem Computer. Sie stehen in main.test.jsx, geschrieben mit node:test und React Testing Library.

Häufige Fehler

Das DOM beim Rendern ändern

export default function Clock() {
  const hour = 3;
  if (hour < 6) {
    document.getElementById('time').className = 'night';
  } else {
    document.getElementById('time').className = 'day';
  }
  return <h1 id="time">{hour}:00</h1>;
}

Was React ausgibt

TypeError: Cannot set properties of null (setting 'className')

Warum, und die Lösung

Während Clock rendert, steht sein <h1> noch nicht auf der Seite, getElementById gibt also null zurück, und der Lauf gibt Uncaught TypeError aus (Formulierung von Chrome und Node.js; Firefox und Safari formulieren es anders). Das DOM zu ändern ist ohnehin eine Nebenwirkung. Berechnen Sie die Klasse und geben Sie sie im JSX zurück: <h1 id="time" className={hour < 6 ? 'night' : 'day'}>.

Etwas zu den children hinzufügen, die eine Komponente bekommen hat

function Menu({ children }) {
  children.push(<li key="help">Help</li>);
  return <ul>{children}</ul>;
}

export default function App() {
  return (
    <Menu>
      <li>Home</li>
      <li>Blog</li>
    </Menu>
  );
}

Was React ausgibt

TypeError: Cannot add property 2, object is not extensible

Warum, und die Lösung

children ist ein Prop, und Props sind Eingaben, die eine Komponente nicht ändern darf. In der Entwicklung friert React das children-Array ein, deshalb wirft push (Formulierung von Chrome und Node.js). Fügen Sie das Item stattdessen im JSX hinzu: return <ul>{children}<li>Help</li></ul>;.

Ein Import-Zyklus im Modulbaum

import Profile from './Profile.jsx';

export const SIZE = 64;

export default function App() {
  return <Profile name="Ada Lovelace" />;
}

Was React ausgibt

ReferenceError: Cannot access 'SIZE' before initialization

Warum, und die Lösung

main.jsx importiert Profile.jsx, und Profile.jsx importiert main.jsx: ein Zyklus im Modul-Abhängigkeitsbaum. JavaScript führt Profile.jsx zuerst aus, bevor main.jsx SIZE definiert hat (Formulierung von Chrome und Node.js; Firefox und Safari formulieren es anders). Halten Sie den Abhängigkeitsbaum einseitig: Verschieben Sie SIZE in ein eigenes Modul, etwa sizes.js, und importieren Sie es in beiden Dateien.

React im Browser: React 19.3.0 (Entwicklungs-Build) und React Testing Library, MIT, von dieser Website ausgeliefert; JSX übersetzt TypeScript 6.0.3, Apache-2.0. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Eine Komponente ist eine Formel

react.dev: React geht davon aus, dass jede Komponente, die Sie schreiben, eine reine Funktion ist. Rein heißt zweierlei. Sie kümmert sich um ihre eigenen Angelegenheiten: Sie ändert keine Variablen oder Objekte, die vor ihrem Aufruf schon existierten, etwa einen Zähler außerhalb der Komponente, ein importiertes Array oder ihre Props. Und dieselben Eingaben ergeben dieselbe Ausgabe: Recipe mit drinkers={2} gibt immer dasselbe JSX zurück. Was Sie gerade erst angelegt haben, dürfen Sie ändern: In ein Array, das in der Komponente entsteht, darf beim Rendern gepusht werden. react.dev nennt das lokale Mutation. Ein importiertes Array an Ort und Stelle zu sortieren ist nicht lokal: Kopieren Sie zuerst, [...people].sort(…).

Strict Mode rendert doppelt, um Unreinheit zu fangen

In der Entwicklung ruft Strict Mode jede Komponentenfunktion zweimal auf. Vites Vorlage und der Runner dieses Kurses schalten ihn ein, deshalb zeigt ihn hier jeder Lauf. Eine reine Komponente merkt nichts davon: Sie berechnet zweimal dasselbe JSX. Eine unreine schon: Zählt Ticket einen Zähler außerhalb hoch, zeigen drei Tickets #2, #4 und #6 statt #1, #2 und #3. In Produktion wirkt Strict Mode nicht. Änderungen, die geschehen müssen, Nebenwirkungen, gehören in Event-Handler: React führt sie beim Klick aus, nicht beim Rendern, und nur einmal pro Klick. useEffect ist der letzte Ausweg.

Ihre App als zwei Bäume

Der Render-Baum hat einen Knoten pro Komponente: Die Wurzel ist die Wurzelkomponente, hier Gallery, und jeder Pfeil zeigt von der Eltern- zur Kindkomponente. HTML-Tags gehören nicht dazu. Top-Level-Komponenten liegen nahe der Wurzel; Blattkomponenten (leaf components) haben keine Kindkomponenten und rendern oft neu. Mit bedingtem Rendern kann der Baum von Rendern zu Rendern anders aussehen. Der Modul-Abhängigkeitsbaum hat einen Knoten pro Modul, auch für Datendateien wie data.js, und einen Zweig pro Import. main.jsx ist seine Wurzel. Bundler entscheiden mit ihm, welcher Code ausgeliefert wird.

Quellen

Zuletzt geprüft am 5. Oktober 2026