Zum Inhalt springen

Aufgabe 07-6a - Quiz reloaded

Zu Zen-Modus wechseln
  • Du modellierst den Zustand einer kleinen Anwendung vollständig, bevor du die erste Codezeile schreibst.
  • Du setzt einen Ablauf mit Phasen ausschließlich über State und bedingtes Rendern um.
  • Du kapselst wiederverwendbare Logik in einem eigenen Hook und weist nach, dass er in einem zweiten Kontext unverändert arbeitet.
  • Du vergleichst den deklarativen mit dem imperativen Ansatz an einer konkreten Erweiterung.

Ein Quiz ist eine kleine Maschine mit klaren Zuständen: Es läuft, es zeigt Rückmeldung, es ist fertig. Wer diese Zustände vorher aufschreibt, hat das Programm im Kopf schon gebaut; wer gleich lostippt, verheddert sich in Wenn-dann-Ketten.

Du baust die erste vollständige React-Anwendung: ein Multiple-Choice-Quiz mit Phasen, Punktestand und Auswertung (siehe Kapitel React-Grundlagen, alle Einheiten, besonders 7.4 bis 7.6). Die Übung führt zusammen, was die React-Wochen aufgebaut haben, und ergänzt die Hook-Familie um ein selbstgebautes Mitglied. Sie passt nach Einheit 7.6 (Die Hook-Familie).

Projektordner anlegen siehe https://qiz.at/projektordner

  • Lies vorher kurz https://qiz.at/assignmentsabgabe durch.

  • Nur vollständige Abgaben (siehe https://qiz.at/rules) können positiv bewertet werden.

  • Abgabe- und Testdetails siehe https://qiz.at/assignmentsabgabe

  • Die Übung liegt als Unterordner uebung-07-6a in deinem Jahres-Repository SJ26_27_4AHITM_SEW_<Benutzername>_exercises, angelegt wie uebung-07-1a mit npm create vite@latest uebung-07-6a -- --template react-ts. Du arbeitest allein.

  • Leg src/components/Quiz.tsx an und render die Komponente aus App.tsx. Bis die Fragendatei steht, genügt eine kurze Platzhalterliste direkt in App.tsx:

    import { Quiz } from "./components/Quiz";
    // import { questions } from "./data/questions"; // wire this up later
    const questions = [
    { text: "2 + 2 = ?", answers: ["3", "4", "5"], correctIndex: 1 },
    ];
    export default function App() {
    return (
    <main>
    <Quiz questions={questions} />
    </main>
    );
    }
  • Verbindliche Regel: kein direkter DOM-Zugriff (document. ist in den Komponenten verboten); jede Ansicht folgt aus Phase und State über bedingtes Rendern. Schriftliche Antworten stehen als Kommentar im Code oder in einer Markdown-Datei im Projekt. npm run build muss fehlerfrei durchlaufen; node_modules/ kommt nicht ins Repository.

  • Für die Stufe PlusPlus brauchst du die imperative Vanilla-JavaScript-Fassung desselben Quiz: quiz-vanilla.zip mit quiz-vanilla.html und quiz-vanilla.js. Entpack das Archiv so, dass beide Dateien im selben Ordner liegen; danach genügt ein Doppelklick auf die HTML-Datei, es braucht keinen Server.

  • Zustandsmodell zuerst: Bestimm vor der ersten Codezeile die State-Variablen des Quiz (Kandidaten: Nummer der aktuellen Frage, Punktestand, gewählte Antwort, Phase als Union Type "running" | "feedback" | "finished"). Prüf jede gegen die Regel aus Aufgabe 07-4a: echter State oder ableitbar? Halte das Ergebnis als Tabelle in STATE_MODEL.md fest.
  • Skizzier den Ablauf als Zustandsdiagramm: Phasen als Zustände, Benutzeraktionen als Übergänge (Antwort geklickt, Weiter, Nochmal). Die Skizze ist Teil der Abgabe.
  • Leg interface Question (Text, drei Antworten, Index der richtigen) und mindestens acht Fragen in src/data/questions.ts an und ersetz die Platzhalterliste.
  • Ablauf: Eine Frage mit drei Antwort-Buttons; nach dem Klick Phase feedback (richtige Antwort grün, falsch gewählte rot, alle Buttons deaktiviert), Button “Weiter”; nach der letzten Frage Phase finished mit “n von 8 richtig” und “Nochmal”.
  • Eine Fortschrittszeile “Frage 3 von 8 - Punkte: 2” ist immer aktuell, ohne dass irgendwo “aktualisiert” wird. Halte als Kommentar fest, warum das im deklarativen Modell automatisch gilt.

Tipp: Zwei Booleans isFinished und showFeedback ergeben vier Kombinationen, von denen nur drei Sinn ergeben; der Union Type lässt den unmöglichen Zustand gar nicht erst zu. “Objects are not valid as a React child” heißt: Du renderst ein ganzes Objekt statt eines Felds, etwa {question} statt {question.text}. Der Punktestand hinkt hinterher? Dann fehlt die Funktionsform des Setters.

Vollständig, wenn:

  • npm run build ohne TypeScript-Fehler durchläuft und das Quiz von der ersten bis zur letzten Frage und über “Nochmal” durchspielbar ist.
  • die Phase ein Union Type ist und es keine zwei Booleans für denselben Sachverhalt gibt.
  • die Fortschrittszeile abgeleitet ist und kein document.-Zugriff vorkommt; Buttons werden über disabled und className aus dem State gesteuert.
  • interface Question typisiert ist, mindestens acht Fragen in src/data/questions.ts liegen und Zustandstabelle und Zustandsdiagramm zur Umsetzung passen.

Wie unter Allgemein, dazu ein Foto des Zustandsdiagramms.

Für alle, die in der Einheit mit den Schritten oben fertig sind bzw. optional (= freiwillig), wenn du in der Einheit nicht fertig wirst.

Erweitere (siehe Erweitern in https://qiz.at/rules) den vollständigen Teil der vorherigen Stufe dieses Projekts (ändere oder lösche nur, was für diese Aufgabe unbedingt notwendig ist):

  • Die Auswertung zeigt zusätzlich den Prozentwert und einen Kommentar zur Leistung (etwa ab 80 Prozent “Sehr gut!”); beides ist abgeleitet.
  • Der beste Prozentwert soll ein Neuladen der Seite überleben: Schreib den Hook useLocalStorage(key, initial) nach dem Muster aus Einheit 7.6 in src/hooks/useLocalStorage.ts und verwende ihn für ein “Best: n %” in der Auswertung.
  • Der Hook ist quizfrei formuliert (kein Quiz-Begriff im Hook) und muss unverändert in einer anderen Anwendung funktionieren.
  • Weis nach, dass der Bestwert wirklich gespeichert wird: einmal durchspielen, Seite neu laden, den Eintrag in den DevTools unter Application - Local Storage prüfen. Screenshot in die Abgabe.

Tipp: localStorage speichert nur Text; Zahlen kommen als String zurück (Number(...) beim Lesen, String(...) beim Schreiben). Mit der Lazy-Form useState(() => localStorage.getItem(key) ?? initial) liest der Hook den Speicher nur beim ersten Rendern. Prozentwert: Math.round((score / questions.length) * 100).

Vollständig, wenn:

  • Prozentwert und Kommentar abgeleitet sind und keinen eigenen State haben.
  • useLocalStorage in einer eigenen Datei liegt, keinen Quiz-Begriff enthält und die Lazy-Form verwendet.
  • der Bestwert das Neuladen überlebt und der Screenshot das belegt.

Wie unter Allgemein, dazu der Local-Storage-Screenshot.

Für alle, die in der Einheit mit den Schritten oben fertig sind bzw. optional (= freiwillig), wenn du in der Einheit nicht fertig wirst.

Erweitere (siehe Erweitern in https://qiz.at/rules) den vollständigen Teil der vorherigen Stufe dieses Projekts (ändere oder lösche nur, was für diese Aufgabe unbedingt notwendig ist):

  • Bring die ausgeteilte imperative Vanilla-Fassung (siehe Allgemein) lokal zum Laufen und lies den Code.
  • Bau in beide Fassungen dieselbe kleine Erweiterung ein: Nach einer falschen Antwort zeigt eine Zeile “davon richtig: k von n” (richtige Antworten von bisher beantworteten). Protokollier je Fassung, welche Stellen du anfassen musstest.
  • Schreib den Vergleich (fünf bis acht Sätze): Wo steckt im imperativen Ansatz die meiste Arbeit? Welche Fehlerklasse verschwindet deklarativ? Und ehrlich: Gibt es etwas, das imperativ einfacher war?

Tipp: Such in der Vanilla-Fassung nach allen Stellen, an denen progressElement.textContent gesetzt wird. Genau diese Vervielfachung ist der Punkt der Aufgabe.

Vollständig, wenn:

  • beide Fassungen laufen und die Erweiterung in beiden funktioniert.
  • das Protokoll konkrete Stellen je Fassung nennt.
  • der Vergleich die Fehlerklasse benennt und auch auf Vorteile des imperativen Ansatzes eingeht.

Wie unter Allgemein; die erweiterte Vanilla-Fassung liegt im Unterordner vanilla/.

Für alle, die in der Einheit mit den Schritten oben fertig sind bzw. optional (= freiwillig), wenn du in der Einheit nicht fertig wirst.

Erweitere (siehe Erweitern in https://qiz.at/rules) den vollständigen Teil der vorherigen Stufe dieses Projekts (ändere oder lösche nur, was für diese Aufgabe unbedingt notwendig ist):

  • Die Antworten jeder Frage erscheinen in zufälliger Reihenfolge. Begründe als Kommentar, warum die Mischung nicht beim Rendern berechnet werden darf und wo sie stattdessen wohnt, und setz das um. Die Farben der Rückmeldung müssen weiter an der richtigen Antwort hängen.
  • Beweis die Wiederverwendbarkeit deines Hooks: Eine neue Komponente PlayerName merkt sich mit unverändertem useLocalStorage einen Spielernamen über ein kontrolliertes Eingabefeld; der Name überlebt das Neuladen.

Tipp: Was passiert mit einem Math.random() im Rumpf der Komponente, wenn nach dem Klick auf eine Antwort neu gerendert wird? Und was macht StrictMode im Entwicklungsmodus mit jedem Rendern? Als key der Buttons eignet sich der Antworttext, nicht die Position.

Vollständig, wenn:

  • die Reihenfolge pro Frage gemischt ist, während der Feedback-Phase stabil bleibt und die Färbung zur richtigen Antwort passt.
  • die Begründung zur Mischung den Render-Kreislauf benennt.
  • PlayerName den Hook ohne Änderung verwendet und der Name das Neuladen überlebt.

Wie unter Allgemein.

  1. Warum ist die Phase als Union Type besser modelliert als mit zwei Booleans (isFinished, showFeedback)?
  2. Welche Werte des Quiz sind abgeleitet und dürfen keinen eigenen State haben?
  3. Warum sind die Antwort-Buttons in der Feedback-Phase deaktiviert, und wie erreicht man das deklarativ?
  4. Was macht useLocalStorage zu einem Hook und nicht zu einer gewöhnlichen Funktion?
  5. Welche Fehlerklasse des imperativen Ansatzes verhindert React strukturell?

Durchschnittlich bis Wesentlich_vollständig: ca. 100 min

  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules