# ES6 Memory: UI-State begreifen

URL: https://www.mikebild.dev/de/blog/es6-memory-ui-state/

Eine einzelne Memory-Karte hat vier unterscheidbare Zustände: verdeckt, aufgedeckt, gefunden – und gesperrt, solange zwei offene Karten gerade verglichen werden. Im DOM landen davon üblicherweise genau zwei, als CSS-Klassen wie `flipped` und `matched`. Der dritte Zustand steckt implizit in einem laufenden `setTimeout`, der vierte ergibt sich aus der Abwesenheit der anderen. Zwei von vier Zuständen stehen also nirgends explizit – und genau aus diesen beiden stammen die Fehler, die man in kleinen UIs zuverlässig wiederfindet: die dritte Karte, die sich mitten im Vergleich aufdecken lässt, und das Paar, das nach einem hastigen Doppelklick dauerhaft offen liegen bleibt.

Dass mir dieses Zahlenverhältnis – vier Zustände, zwei davon sichtbar notiert – erst richtig aufgefallen ist, als ich ein Spiel gebaut habe, sagt einiges darüber, wie beiläufig wir UI-Zustand sonst behandeln.

## Ein Spiel als Prüfstand

Seit Juni ist ECMAScript 2015 final. Klassen, Module, `const` und `let`, Arrow-Functions, Template-Literals – das steht jetzt im Standard, aber es fühlt sich noch ungewohnt an, wie eine fremde Sprache mit vertrauten Vokabeln. Ich wollte die neue Syntax nicht an Folien-Beispielen durchdeklinieren, sondern an etwas Echtem: klein genug für ein paar Abende, groß genug, dass echte Zustandsfragen auftauchen. Ein Memory-Spiel passt genau in diese Lücke. Sechzehn Karten, acht Paare, ein Zugzähler – mehr Fachlichkeit braucht es nicht.

Das Ergebnis liegt als [es6-memory](https://github.com/MikeBild/es6-memory) auf GitHub. Bewusst ohne Framework: kein jQuery, kein React. Nicht aus Prinzipienreiterei, sondern weil ich wissen wollte, was von den Zustandsproblemen übrig bleibt, wenn kein Werkzeug sie versteckt oder abnimmt. Der Build ist trotzdem zeitgemäß: jspm löst die Abhängigkeiten auf, SystemJS lädt die ES2015-Module, Babel übersetzt die neue Syntax für Browser, die noch nicht so weit sind – und ein kleines `node build` bündelt alles zu einer `memory.min.js`, die sich mit einem einzigen `div` in jede bestehende Seite hängen lässt. Passend zur Woche, in der das Repository entstand: Anfang September ist Node 4.0 erschienen, die erste Version nach der Wiedervereinigung mit io.js, mit V8-Rückenwind für einen Teil der neuen Sprachfeatures. ES2015 ist gerade dabei, vom Papier in die Werkzeuge zu wandern, und genau in diesem Moment lohnt sich ein Projekt, das die Sprache pur benutzt.

## Welche Zustände es wirklich gibt

Der erste Gewinn kam vor der ersten Zeile Code: Zustände aufzählen. Für eine Karte sind es drei, nicht vier – das war die erste Korrektur an meinem eigenen Bild vom Spiel:

- verdeckt: die Rückseite ist sichtbar, die Karte reagiert auf Klicks
- aufgedeckt: das Motiv ist sichtbar, die Karte wartet auf den Vergleich
- gefunden: das Motiv bleibt sichtbar, die Karte reagiert nie wieder

Und der vierte Zustand aus dem Einstieg? „Gesperrt" ist gar kein Kartenzustand. Es ist ein Zustand des Spiels: Solange zwei aufgedeckte Karten verglichen werden – und beim Nicht-Treffer kurz sichtbar bleiben, damit man sie sich merken kann – darf keine weitere Karte umgedreht werden. Dazu kommen der Zugzähler und die abgeleitete Frage, ob das Spiel vorbei ist. Diese Trennung zwischen Karten- und Spielzustand fällt einem nicht ein, solange man in CSS-Klassen denkt. Sie fällt einem sofort ein, wenn man den Zustand als Datenstruktur aufschreiben muss.

```mermaid
flowchart LR
  H[hidden] -->|flipCard| R[revealed]
  R -->|second card<br/>matches| M[matched]
  R -->|no match<br/>after 800 ms| H
  M --> E[game over<br/>when all matched]
```

In ES2015 liest sich das Modell angenehm direkt. Eine Klasse für die Karte, eine Factory für das Spiel – und zum ersten Mal ohne das übliche Prototypen-Geflecht:

```js
// card.js
export class Card {
  constructor(id, motif) {
    this.id = id;
    this.motif = motif;
    this.state = 'hidden'; // 'hidden' | 'revealed' | 'matched'
  }
}

// game.js
import { Card } from './card.js';

export function createGame(motifs) {
  const pairs = motifs.concat(motifs);
  const cards = shuffle(pairs).map((motif, id) => new Card(id, motif));
  return { cards, locked: false, moves: 0 };
}
```

Das ist der komplette Zustand des Spiels: ein Array von Karten mit je einem `state`-Feld, ein `locked`-Flag, ein Zähler. Kein `data-`Attribut, keine Klassenliste, kein verstecktes Timeout, das nebenbei Wahrheit transportiert.

## Wo der Zustand liegt – und wo nicht

Die naheliegende Alternative kenne ich aus genug jQuery-Codebasen der letzten Jahre, auch aus eigenen: Das DOM ist der Zustandsspeicher. Ob eine Karte aufgedeckt ist, entscheidet `classList.contains('flipped')`. Wie viele Karten offen sind, ermittelt ein Selektor. Das funktioniert erstaunlich lange – und kippt dann an immer derselben Stelle: Sobald Timing ins Spiel kommt, gibt es Momente, in denen die Darstellung der Wahrheit hinterherläuft. Die Animation ist noch nicht fertig, die Klasse noch nicht gesetzt, der Selektor zählt falsch. Der Fehler ist dann nicht deterministisch, sondern hängt davon ab, wie schnell jemand klickt.

Der tiefere Punkt: Ein DOM-Knoten vermischt zwei Dinge, die getrennt gehören – was der Fall ist und wie es gerade aussieht. Solange beides identisch ist, merkt man den Unterschied nicht. Interessant wird es genau dann, wenn beides auseinanderfällt, und das tut es bei jedem `setTimeout`, bei jeder Transition, bei jedem Server-Roundtrip. Liegt der Zustand dagegen im Modell, ist er zu jedem Zeitpunkt eindeutig, unabhängig davon, was der Browser gerade zeichnet. Und er ist ohne Browser testbar: `createGame`, zweimal `flipCard`, Assertion auf `state` – das läuft in Node in Millisekunden, ganz ohne DOM-Attrappe.

## Eine Übergangsfunktion statt verstreuter Toggles

Die zweite Entscheidung folgt aus der ersten: Wenn der Zustand an einem Ort liegt, sollte er auch nur an einem Ort verändert werden. Nicht drei Event-Handler, die jeweils ein bisschen Klassenlogik machen, sondern eine einzige Funktion, die alle Spielregeln kennt:

```js
// game.js
export function flipCard(game, cardId, onSettled) {
  const card = game.cards.find(c => c.id === cardId);
  if (game.locked || !card || card.state !== 'hidden') return;

  card.state = 'revealed';
  const revealed = game.cards.filter(c => c.state === 'revealed');
  if (revealed.length < 2) return;

  game.locked = true;
  game.moves += 1;
  const [first, second] = revealed;
  const isMatch = first.motif === second.motif;

  setTimeout(() => {
    first.state = second.state = isMatch ? 'matched' : 'hidden';
    game.locked = false;
    onSettled();
  }, isMatch ? 300 : 800);
}
```

Die erste Zeile nach dem `find` ist der eigentliche Ertrag des ganzen Umbaus. `game.locked || card.state !== 'hidden'` erledigt beide Fehler aus dem Einstieg in einem Ausdruck: Die dritte Karte während des Vergleichs prallt am `locked` ab, der Doppelklick auf dieselbe Karte am Kartenzustand. In der DOM-Variante waren das zwei getrennte Symptome an zwei Stellen im Code, jedes mit eigenem Workaround. Im Modell sind es zwei Facetten derselben Regel: Aufdecken ist nur erlaubt, wenn das Spiel offen und die Karte verdeckt ist.

Mir ist beim Schreiben dieser Funktion noch etwas aufgefallen: Sie ist die Stelle, an der man über das Spiel redet. Als eine Kollegin im Review fragte, ob ein aufgedecktes Paar sofort oder verzögert einrasten soll, konnte ich auf zwei Zeilen zeigen. Die Frage „Wer darf diesen Zustand ändern?" hat im Kleinen dieselbe Antwort verdient wie im Großen: möglichst genau eine Funktion, und die trägt einen Namen aus der Fachsprache.

## Rendern in eine Richtung

Bleibt die dritte Frage: Wie kommt der Zustand auf den Bildschirm? Die Antwort, die sich beim Bauen als tragfähig erwiesen hat, ist fast unangenehm simpel – nach jeder Änderung wird das komplette Brett neu gezeichnet:

```js
// render.js
export function render(game, root) {
  root.innerHTML = `
    <p class="moves">Moves: ${game.moves}</p>
    <ul class="board">
      ${game.cards.map(card => `
        <li class="card ${card.state}" data-id="${card.id}">
          ${card.state === 'hidden' ? '' : card.motif}
        </li>`).join('')}
    </ul>`;
}
```

Template-Literals machen diese Art von Rendering zum ersten Mal angenehm. Vor ES2015 hieß das String-Konkatenation über zwanzig Zeilen oder eine Template-Bibliothek als Abhängigkeit; jetzt ist es ein mehrzeiliger Ausdruck mit Interpolation, direkt in der Sprache. Die Verdrahtung dazu ist ein einziger delegierter Listener:

```js
// main.js
import { createGame, flipCard } from './game.js';
import { render } from './render.js';

const root = document.querySelector('.memory');
const game = createGame(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);

const update = () => render(game, root);

root.addEventListener('click', event => {
  if (!event.target.classList.contains('card')) return;
  flipCard(game, Number(event.target.dataset.id), update);
  update();
});

update();
```

Damit fließt alles in eine Richtung: Ein Klick ist nur noch eine Meldung an die Übergangsfunktion, die Übergangsfunktion ändert das Modell, das Modell wird gerendert. Kein Handler fasst je das DOM an, kein Renderer ändert je den Zustand.

```mermaid
flowchart LR
  E[Klick-Event] --> T[flipCard<br/>Übergangsfunktion]
  T --> S[(Spielzustand<br/>im Modell)]
  S --> R[render]
  R --> D[DOM]
  D -. meldet nur Events,<br/>hält keinen Zustand .-> E
```

Der reflexhafte Einwand lautet: Alles neu rendern, ist das nicht verschwenderisch? Ich habe es gemessen, statt es zu glauben. Siebzehn Elemente per `innerHTML` zu ersetzen kostet in Chrome deutlich weniger als eine Millisekunde – bei einer Interaktion, die höchstens zweimal pro Sekunde stattfindet. Die Grenze existiert, aber sie liegt weit weg von sechzehn Karten: Bei Hunderten Knoten, Formularfokus oder Scroll-Positionen, die den Neuaufbau nicht überleben, trägt der Ansatz nicht mehr. Genau an dieser Grenze setzt React an, das mit Version 0.13 gerade viel Aufmerksamkeit bekommt: Es behält das Prinzip „Zustand rein, Darstellung raus" bei und macht nur den Schreibweg ins DOM schlau, indem es Unterschiede berechnet, statt alles zu ersetzen. Wer das Memory-Spiel von Hand gebaut hat, liest die React-Dokumentation anders – als Optimierung eines Modells, das man schon verstanden hat, statt als Magie.

## Was ein Framework abnimmt – und was nicht

Das ist für mich die eigentliche Erkenntnis aus dem Projekt, und sie hat mit ES2015 nur am Rande zu tun. Ein Framework nimmt einem eine Menge ab:

- effizientes Aktualisieren des DOM, wenn das Neuzeichnen zu teuer wird
- Zerlegung der Oberfläche in komponierbare, wiederverwendbare Teile
- Verdrahtung von Events, Templates und Daten ohne Eigenbau-Konventionen

Was kein Framework abnimmt, sind die drei Fragen, die dieses kleine Spiel erzwungen hat: Welche Zustände gibt es überhaupt – und welche davon gehören zur Karte, welche zum Spiel? Wo liegen sie – im Modell oder verschmiert über Klassenlisten und Timeouts? Und wer darf sie ändern – eine benannte Übergangsfunktion oder jeder Handler ein bisschen? Wer diese Fragen nicht beantwortet, bekommt mit Framework dieselben Fehler wie ohne, nur besser verpackt. Duplizierter Zustand zwischen Komponente und DOM sieht in JSX ordentlicher aus als in jQuery, bleibt aber duplizierter Zustand. Am Rande: Aus der React-Gemeinde kommt seit dem Sommer mit Redux ein Bibliotheks-Experiment, das genau diese Übergangsfunktion zum zentralen Baustein erklärt – ein Hinweis darauf, dass die Idee nicht am Spielzeugbeispiel endet.

## Was ich daraus mitnehme

Das Spiel selbst ist ein Nachmittagsvergnügen, ein Prototyp mit acht Motiven und einem Zugzähler. Der Wert steckt in dem, was es erzwungen hat. Erstens: Zustände aufzählen, bevor Code entsteht – schon die Inventur hat mein Bild korrigiert, weil „gesperrt" auf der falschen Ebene vermutet war. Zweitens: Der Zustand gehört ins Modell; das DOM ist eine Projektion davon und taugt nicht als Speicher, sobald Timing ins Spiel kommt. Drittens: eine Übergangsfunktion, die alle Regeln kennt, statt Toggles, die über Handler verteilt sind – dort finden Reviews statt, dort steht die Fachsprache, dort verschwinden zwei Bugs in einer Guard-Zeile. Und die neue Sprache? ES2015 hat das Ganze nicht möglich, aber angenehm gemacht: Klassen für das Modell, Module für die Schnitte, Template-Literals für das Rendering. Babel und jspm sorgen dafür, dass es heute schon in echten Browsern läuft. Das nächste größere Frontend werde ich vermutlich nicht ohne Framework bauen – aber ich werde genauer wissen, welchen Teil der Arbeit das Framework wirklich übernimmt und welcher Teil vorher im eigenen Kopf passieren muss.

## Weiterführende Quellen

- Repository es6-memory: https://github.com/MikeBild/es6-memory
- MDN: Classes (JavaScript-Referenz): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
- MDN: Template literals: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
- Babel, der JavaScript-Compiler für ES2015: https://babeljs.io/

Die Links helfen beim Nachschlagen der Sprachfeatures – die Entscheidung, wo Zustand liegt und wer ihn ändert, nehmen sie einem nicht ab.
