Tutorial: Einführung in React
Für dieses Tutorial benötigst Du keine Vorkenntnisse in React.
Bevor wir mit dem Tutorial anfangen
Wir werden im Laufe dieses Tutorials ein kleines Spiel programmieren. ** Vielleicht neigst du dazu, es zu überspringen, weil du keine Spiele programmieren willst — aber gib ihm doch eine Chance.** Die Techniken, die du in diesem Tutorial lernen wirst, sind notwendig um eine React-App zu erstellen. Wenn du das Tutorial bewältigst, wird dir das ein tiefes Verständnis von React geben.
Tipp
Dieses Tutorial ist gestaltet für jene, die learn by doing bevorzugen. Solltest du es bevorzugen, die Konzepte von Grund auf zu lernen, schau doch in unsere Schritt für Schritt Anleitung. Das Tutorial und die Anleitung ergänzen sich gegenseitig.
Dieses Tutorial ist in mehrere Abschnitte aufgeteilt:
- Das Setup für das Tutorial gibt dir einen Ausgangspunkt um das Tutorial durchzuführen.
- Die Übersicht bringt dir die Grundlagen von React bei: Components, Props und State.
- Das Spiel fertigstellen zeigt dir die am häufigsten vorkommenden Techniken während der Entwicklung mit React.
- Zeitreisen hinzufügen gibt dir einen tieferen Einblick in die einzigartigen Stärken von React.
Du musst nicht sofort alle Abschnitte abschließen, um einen Mehrwert aus dem Tutorial zu ziehen. Versuche so weit zu kommen, wie du kannst, selbst wenn es nur ein oder zwei Abschnitte sind.
Was werden wir erstellen?
In diesem Tutorial werden wir dir zeigen, wie du ein interaktives Tic-Tac-Toe-Spiel mit React erstellen kannst.
Hier kannst du sehen, was wir erstellen werden: Finales Ergebnis. Sollte der Code keinen Sinn für dich ergeben oder solltest du dich mit der Syntax nicht auskennen, kein Problem! Das Ziel dieses Tutorials ist es, dir zu helfen React und dessen Syntax zu verstehen.
Wir empfehlen dir, dass du dir das Tic-Tac-Toe-Spiel anschaust, bevor du mit dem Tutorial weitermachst. Eines der Features, die du bemerken wirst, ist, dass sich rechts vom Spielbrett eine nummerierte Liste befindet. Diese Liste zeigt alle getätigten Züge an und wird mit dem Spielfortschritt aktualisiert.
Du kannst das Tic-Tac-Toe-Spiel schließen, sobald du dich damit vertraut gemacht hast. Wir werden in diesem Tutorial mit einem einfacheren Template beginnen. Im nächsten Schritt helfen wir dir alles einzurichten, damit du anfangen kannst, das Spiel zu erstellen.
Voraussetzungen
Wir nehmen an, dass du bereits ein wenig mit HTML und JavaScript vertraut bist. Selbst wenn du vorher eine andere Sprache erlernt hast, solltest du trotzdem in der Lage sein, folgen zu können. Des Weiteren nehmen wir an, dass dir Programmierkonzepte wie Funktionen, Objekte, Arrays und in geringerem Maße auch Klassen ein Begriff sind.
Wenn du die Grundlagen von JavaScript noch einmal wiederholen möchtest, empfehlen wir dir dieses Handbuch zu lesen. Beachte, dass wir auch Features von ES6 nutzen werden — eine aktuelle Version von JavaScript. In diesem Tutorial verwenden wir unter anderem die sogenannte arrow-Funktion, Klassen, let
, und const
Ausdrücke. Schaue dir Babel REPL an, um zu sehen, in was ES6-Code kompiliert wird.
Setup für das Tutorial
Es gibt zwei Möglichkeiten, das Tutorial durchzuführen: Entweder du schreibst den Code direkt im Browser oder du richtest dir eine lokale Entwicklungsumgebung ein.
Möglichkeit 1: Code direkt im Browser schreiben
Das ist der schnellste Weg um direkt anfangen zu können!
Zuerst öffnest du diesen Einstiegscode in einem neuen Tab. Im neuen Tab solltest du ein leeres Tic-Tac-Toe-Spielbrett und React-Code sehen können. Den React-Code werden wir innerhalb dieses Tutorials bearbeiten.
Jetzt kannst du die zweite Setup-Option überspringen und direkt zum Abschnitt Übersicht springen, um eine Übersicht über React zu erhalten.
Möglichkeit 2: Lokale Entwicklungsumgebung
Das ist komplett optional und für dieses Tutorial nicht notwendig!
Optional: Anleitung um lokal in deinem bevorzugten Texteditor mit dem Tutorial weiterzumachen
Dieses Setup benötigt mehr initialen Aufwand, ermöglicht dir jedoch, das Tutorial in einem Editor deiner Wahl durchzuführen. Folgende Schritte sind dafür notwendig:
- Stelle sicher, dass du eine aktuelle Version von Node.js installiert hast.
- Folge den Installationsanweisungen für Create React App um ein neues Projekt zu erstellen.
npx create-react-app meine-app
- Lösche alle Dateien innerhalb des
src/
-Ordners des neuen Projektes.
Hinweis:
Lösche nicht den gesamten
src
-Ordner sondern lediglich die Dateien innerhalb des Verzeichnisses. Wir werden die Standard-Quellcode-Dateien im nächsten Schritt mit Beispielen für dieses Projekt ersetzen.
cd meine-app
cd src
# Wenn du einen Mac oder Linux benutzt:
rm -f *
# Oder wenn du Windows nutzt:
del *
# Wechsle danach zurück zum Projekt-Verzeichnis
cd ..
- Füge eine Datei mit dem Namen
index.css
in densrc/
-Ordner mit diesem CSS-Code ein. - Füge eine Datei mit dem Namen
index.js
in densrc/
-Ordner mit diesem JS-Code ein. - Schreibe diese drei Zeilen an den Anfang von
index.js
imsrc/
-Ordner:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
Wenn du jetzt npm start
in deinem Projektverzeichnis ausführst und dann im Browser http://localhost:3000
aufrufst, solltest du ein leeres Tic-Tac-Toe-Feld sehen.
Wir empfehlen dir diese Anweisungen zu befolgen, um Syntax-Highlighting für deinen Editor zu konfigurieren.
Hilfe, ich hänge fest!
Wenn du nicht mehr weiterkommst, schau am besten in die Community-Support-Ressourcen. Insbesondere ist der Reactiflux Chat ein guter Weg um schnell Hilfe zu bekommen. Solltest du keine Antwort bekommen, oder weiterhin festhängen, erstelle ein Issue-Ticket und wir werden dich unterstützen.
Übersicht
Da du jetzt alles vorbereitet hast, lass uns erstmal einen Überblick über React bekommen!
Was ist React?
React ist eine deklarative, effiziente und flexible JavaScript-Bibliothek für das Erstellen von Benutzeroberflächen. Es ermöglicht dir, komplexe Oberflächen aus kleinen isolierten Code-Schnipseln, sogenannten “Komponenten” (engl. components), zusammenzustellen.
React bietet ein paar unterschiedliche Arten von Komponenten an, aber wir beginnen mit den React.Component
Subklassen:
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shoppingliste für {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
// Anwendungsbeispiel: <ShoppingList name="Mark" />
Wir kommen bald zu den merkwürdigen XML-ähnlichen Tags. Wir nutzen Komponenten um React mitzuteilen, was wir dargestellt haben wollen. Wenn sich unsere Daten ändern wird React die Komponenten effizient aktualisieren und neu darstellen.
In unserem Beispiel ist ShoppingList eine React Komponenten-Klasse bzw. ein React Komponententyp. Eine Komponente nimmt Parameter, sogenannte props
(kurz für engl. “properties” - Eigenschaften) entgegen und gibt über die render
-Methode eine Darstellungs-Hierarchie zurück.
Die render
-Methode gibt eine Beschreibung von dem zurück, was du auf dem Bildschirm sehen willst. React nimmt diese Beschreibung und zeigt das Ergebnis an. Genauergesagt gibt render
ein React-Element zurück, welches eine leichtgewichtige Beschreibung von dem ist, was dargestellt werden soll. Die meisten React-Entwickler nutzen eine spezielle Syntax namens “JSX” welche es leichter macht, solche Strukturen zu schreiben. Die <div />
-Syntax wird während des Builds in ein React.createElement('div')
umgewandelt. Das Beispiel oben entspricht:
return React.createElement('div', {className: 'shopping-list'},
React.createElement('h1', /* ... h1 Kindelemente ... */),
React.createElement('ul', /* ... ul Kindelemente ... */)
);
Wenn du mehr über createElement()
wissen willst, findest du eine detaillierte Beschreibung in der API Referenz. Jedoch werden wir createElement()
in diesem Tutorial nicht verwenden. Stattdessen werden wir weiterhin JSX nutzen.
JSX hat den vollen Funktionsumfang von JavaScript. Du kannst jeden JavaScript-Ausdruck innerhalb von Klammern in JSX benutzen. Jedes React-Element ist ein JavaScript-Objekt, welches in eine Variable gespeichert oder innerhalb des Programms hin- und hergereicht werden kann.
Die ShoppingList
-Kompontente von oben stellt nur native DOM-Komponenten wie <div />
und <li />
dar. Dennoch kannst du auch eigene React-Komponenten zusammenstellen und darstellen. Wir können jetzt zum Beispiel immer auf die Shopping-Liste verweisen indem wir <ShoppingList />
schreiben. Jede React-Komponente ist abgekapselt und kann eigenständig operieren; das erlaubt dir, komplexe Benutzeroberflächen aus einfachen Komponenten zu kreieren.
Untersuchen des Einstiegscodes
Wenn du an dem Tutorial in deinem Browser arbeitest, öffne diesen Code in einem neuen Tab: Einstiegscode. Wenn du lokal an dem Tutorial arbeitest, öffne stattdessen src/index.js
in deinem Projektverzeichnis (du hast diese Datei bereits während des Setups angelegt).
Dieser Einstiegscode ist die Grundlage auf der wir aufbauen. Wir haben bereits den CSS-Code vorbereitet, sodass du dich nur auf das Lernen von React und das Programmieren des Tic-Tac-Toe-Spiels fokussieren musst.
Beim Betrachten des Codes wirst du feststellen, dass wir drei React-Kompontenten haben:
- Square (Quadrat)
- Board (Spielbrett)
- Game (Spiel)
Die Square-Komponente visualisiert einen einzelnen <button>
und das Board visualisiert 9 Quadrate. Die Game-Komponente visualisiert ein Spielbrett mit Platzhalter-Werten, welche wir später modifizieren werden. Derzeit gibt es keine interaktiven Komponenten.
Daten über Props weitergeben
Um einen ersten Schritt zu wagen, werden wir erstmal probieren Daten von der Board-Komponente in die Square-Komponente weiterzugeben.
Wir empfehlen dir, während du das Tutorial durcharbeitest, den Code selbst zu tippen, statt ihn zu kopieren und einzufügen. Dies hilft dir, dein Muskelgedächtnis (Muscle Memory) zu trainieren und ein besseres Verständnis zu entwickeln.
Ändere den Code in der renderSquare
-Methode der Board-Komponente so, dass eine Prop namens value
an die Square-Komponente weitergereicht wird:
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />; }
}
Ersetze in der render
-Methode der Square-Komponente das {/* TODO */}
mit {this.props.value}
, sodass der Wert angezeigt werden kann:
class Square extends React.Component {
render() {
return (
<button className="square">
{this.props.value} </button>
);
}
}
Vorher:
Nachher: Du solltest in jedem Quadrat eine Nummer sehen.
Schau dir den bis jetzt vorhandenen Code an
Glückwunsch! Du hast gerade von der Eltern-Board-Komponente an die Kind-Square-Komponente “eine Prop weitergegeben”. Der Informationsfluss in React wird durch das Weitergeben von Props der Eltern an Kinder realisiert.
Erstellen einer interaktiven Komponente
Nun werden wir ein Quadrat mit einem “X” füllen, wenn es geklickt wird.
Zuerst werden wir den Button-Tag, der von der render()
-Methode der Square-Komponente zurückgegeben wird, in folgendes ändern:
class Square extends React.Component {
render() {
return (
<button className="square" onClick={function() { console.log('click'); }}> {this.props.value}
</button>
);
}
}
Wenn du jetzt auf ein Quadrat klickst, solltest du in der devtools-Konsole deines Browsers ‘click’ sehen.
Hinweis
Um weniger schreiben zu müssen und das verwirrende Verhalten von
this
zu vermeiden, verwenden wir die arrow-Funktion-Syntax für Event-Handler hier und weiter unten:class Square extends React.Component { render() { return ( <button className="square" onClick={() => console.log('click')}> {this.props.value} </button> ); } }
Wenn du
onClick={() => alert('click')}
betrachtest, kannst du feststellen, dass wir deronClick
-Prop eine Funktion übergeben. React ruft diese Funktion nur nach einem Klick auf. Ein häufiger Fehler ist,() =>
zu vergessen und stattdessenonClick={alert('click')}
zu schreiben. Das führt dazu, dass alert jedes Mal aufgerufen wird, wenn die Komponente neu rendert.
Als nächstes möchten wir, dass sich die Square-Kompontente daran “erinnern” kann, dass sie geklickt wurde und befüllen sie mit einem “X”. Damit Komponenten sich an Dinge “erinnern” können, nutzen sie einen State.
React-Komponenten können einen State haben, indem man this.state
in ihren Konstruktoren setzt. this.state
sollte als private Eigenschaft der React-Komponente verstanden werden, in der es definiert wurde. Nun speichern wir den aktuellen Wert des Quadrats in this.state
und ändern ihn wenn das Quadrat geklickt wird.
Zuerst fügen wir einen Konstruktor zur Klasse hinzu um den State zu initialisieren:
class Square extends React.Component {
constructor(props) { super(props); this.state = { value: null, }; }
render() {
return (
<button className="square" onClick={() => console.log('click')}>
{this.props.value}
</button>
);
}
}
Hinweis
In JavaScript-Klassen musst du immer
super
aufrufen wenn du den Konstruktor einer Subklasse definierst. Alle React-Komponenten-Klassen, die einenconstructor
haben, sollten mit einemsuper(props)
-Aufruf starten.
Jetzt werden wir die render
-Methode der Square-Komponente anpassen, um den aktuellen Wert von State darzustellen wenn sie geklickt wird:
- Ersetze im
<button>
-Tagthis.props.value
mitthis.state.value
. - Ersetze den
onClick={...}
Event-Handler mitonClick={() => this.setState({value: 'X'})}
. - Schreibe die Props
className
undonClick
in zwei separate Zeilen, um die Lesbarkeit zu verbessern.
Nachdem wir diese Änderungen durchgeführt haben, wird der von Squares render
-Methode zurückgegebene <button>
-Tag wie folgt aussehen:
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square" onClick={() => this.setState({value: 'X'})} >
{this.state.value} </button>
);
}
}
Der Aufruf von this.setState
durch einen onClick
-Handler in Squares render
-Methode teilt React mit, dass es die Square-Komponente neu rendern soll, sobald ihr <button>
geklickt wird. Nach der Aktualisierung der Square-Komponente ist this.state.value
'X'
. Dadurch sehen wir ein X
auf dem Spielbrett. Wenn du auf irgendein Feld klickst, sollte ein X
erscheinen.
Wenn du setState
in einer Komponente aufrufst, wird React automatisch alle Kindelemente aktualisieren.
Schau dir den bis jetzt vorhandenen Code an
Entwicklerwerkzeuge
Die React-DevTools-Erweiterung für Chrome und Firefox ermöglicht es dir, den Baum der React-Komponenten in den Entwicklerwerkzeugen deines Browsers anzusehen.
Die React-DevTools ermöglichen es dir, Props und State deiner React-Komponente zu prüfen.
Nach der Installation der React-DevTools kannst du mit Rechtsklick auf jedes Element der Seite im Kontextmenü mit “Untersuchen” die Entwicklerwerkzeuge öffnen. Die React-Tabs (“⚛️ Components” und “⚛️ Profiler”) tauchen dann als letzte Tabs ganz rechts auf. Verwende “⚛️ Components”, um den Komponentenbaum zu untersuchen.
Jedoch sind noch ein paar Zusatzschritte notwendig, um sie mit CodePen nutzen zu können:
- Logge dich ein oder registriere dich und bestätige deine E-Mail (Zur Vermeidung von Spam).
- Klicke auf “Fork”.
- Klicke auf “Change View” und wähle dann “Debug mode”.
- Im neuen Tab, der sich öffnet, sollten die Entwicklerwerkzeuge jetzt einen React-Tab anzeigen.
Das Spiel fertigstellen
Jetzt verfügen wir über die Grundbausteine unseres Tic-Tac-Toe-Spiels. Um das Spiel zu vervollständigen, müssen wir abwechselnd die ‘X’e und ‘O’s auf dem Spielfeld platzieren. Außerdem müssen wir noch eine Möglichkeit finden, um den Gewinner festzustellen.
Den State hochziehen
Zurzeit verwaltet jede Square-Komponente den Spiel-State. Um zu prüfen, ob es einen Gewinner gibt, werden wir den Wert jedes der 9 Quadrate an einem Ort verwalten.
Man könnte denken, dass die Board-Komponente einfach jede Square-Komponente zu ihrem aktuellen State befragen sollte. Das ist zwar eine mögliche Herangehensweise in React, wir raten jedoch davon ab, da dies zu unverständlichem Code führt, der fehleranfällig und schwer zu überarbeiten ist. Anstelle dessen ist die beste Herangehensweise den aktuellen State in der Eltern-Board-Komponente zu speichern statt in jeder Square-Komponente selbst. Die Board-Komponente kann jeder Square-Komponente durch props mitteilen, was sie anzeigen soll. Genauso haben wir es gemacht, als wir Zahlen an die Square-Komponente übergeben haben.
Um Daten von mehreren Kindern zu sammeln oder um zwei Kind-Komponenten miteinader kommunizieren zu lassen, musst du einen geteilten State in ihrer Elternkomponente deklarieren. Die Elternkomponente kann den State an die Kinder mittels Props zurückreichen. So können Kindkomponenten untereinander und mit der Elternkomponente synchronisiert werden.
Das Hochziehen des States in eine Eltern-Komponente ist üblich, wenn React-Komponenten überarbeitet werden — nutzen wir diese Gelegenheit, um es auszuprobieren.
Füge der Board-Komponente einen Konstruktor hinzu und setze den initialen State auf ein Array mit 9 null
-Einträgen, die den 9 Quadraten entsprechen:
class Board extends React.Component {
constructor(props) { super(props); this.state = { squares: Array(9).fill(null), }; }
renderSquare(i) {
return <Square value={i} />;
}
Wenn wir das Spielfeld später ausfüllen, wird das Array this.state.squares
in etwa so aussehen:
[
'O', null, 'X',
'X', 'X', 'O',
'O', null, null,
]
Die renderSquare
-Methode der Board-Komponente sieht aktuell so aus:
renderSquare(i) {
return <Square value={i} />;
}
Am Anfang haben wir von der Board-Komponente die value
-Prop nach unten weitergegeben, um die Nummern 0 bis 8 in jeder Square-Komponente anzuzeigen. In einem anderen vorherigen Schritt haben wir die Zahlen dann durch ein “X”, bestimmt durch den State der Square-Komponente, ersetzt. Deshalb ignoriert die Square-Komponente gerade die Prop value
, die ihr von der Board-Komponente übergeben wurde.
Wir werden jetzt den Mechanismus zur Weitergabe von Props wieder verwenden. Wir werden die Board-Komponente so anpassen, dass sie jeder einzelnen Square-Komponente ihren aktuellen Wert ('X'
, 'O'
, oder null
) mitteilt. Wir haben bereits das squares
-Array im Konstruktor der Board-Komponente definiert und werden nun die renderSquare
-Methode so anpassen, dass diese daraus liest:
renderSquare(i) {
return <Square value={this.state.squares[i]} />; }
Schau dir den bis jetzt vorhandenen Code an
Jede Square-Komponente erhält nun die Prop value
, welche entweder den Wert 'X'
, 'O'
, oder null
für leere Quadrate enthält.
Als nächstes müssen wir das Verhalten ändern, wenn ein Quadrat geklickt wird. Die Board-Komponente verwaltet nun, welche Quadrate gefüllt sind. Wir müssen jetzt eine Möglichkeit finden, damit die Square-Komponente den State in der Board-Komponente anpassen kann. Der State ist in der Komponente, in der er definiert wurde, eine private Variable. Somit können wir den State der Board-Komponente nicht direkt von der Square-Komponente aus aktualisieren.
Stattdessen übergeben wir eine Funktion von der Board-Komponente an die Square-Komponente und lassen die Square-Komponente diese aufrufen, wenn ein Quadrat geklickt wird. Wir ändern die “renderSquare”-Methode in der Board-Komponente folgendermaßen:
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)} />
);
}
Hinweis
Wir haben das zurückgegebene Element aus Gründen der Lesbarkeit in mehrere Zeilen aufgeteilt. Außerdem haben wir Klammern hinzugefügt, damit JavaScript kein Semikolon nach
return
einfügt und unseren Code kaputt macht.
Jetzt geben wir zwei Props von der Board-Komponente an die Square-Komponente weiter: value
und onClick
. Die onClick
-Prop ist eine Funktion, die von der Square-Komponente immer aufgerufen wird, wenn sie geklickt wird. Dafür nehmen wir folgende Änderungen an der Square-Komponente vor:
- Ersetzen von
this.state.value
durchthis.props.value
in derrender
-Methode der Square-Komponente. - Ersetzen von
this.setState()
durchthis.props.onClick()
in derrender
-Methode der Square-Komponente. - Löschen des
constructor
in der Square-Komponente, da diese den State des Spiels nicht mehr verfolgt
Nach diesen Änderungen sieht unsere Square-Komponente so aus:
class Square extends React.Component { render() { return (
<button
className="square"
onClick={() => this.props.onClick()} >
{this.props.value} </button>
);
}
}
Wenn ein Quadrat geklickt wird, wird die von der Board-Komponente bereitgestellte onClick
-Funktion aufgerufen. Nachfolgend befindet sich eine Zusammenfassung, wie das erreicht wird:
- Die
onClick
-Prop der nativen DOM-<button>
-Komponente weist React an, einen Click-Event-Listener einzurichten. - Wenn der Button geklickt wird, ruft React den
onClick
-Event-Handler auf, der in derrender()
-Methode der Square-Komponente definiert ist. - Dieser Event-Handler ruft
this.props.onClick()
auf. DieonClick
-Prop der Square-Komponente wurde von der Board-Komponente festgelegt. - Da die Board-Komponente der Square-Komponente
onClick={() => this.handleClick(i)}
übergeben hat, ruft die Square-Komponentethis.handleClick(i)
der Board-Komponente auf, wenn sie geklickt wird. - Wir haben die
handleClick()
-Methode noch nicht definiert, daher stürzt unser Code ab. Wenn du jetzt auf ein Quadrat klickst, solltest du einen roten Fehlerbildschirm sehen, der etwas sagt wie “this.handleClick is not a function”.
Hinweis
Das
onClick
-Attribut des DOM-<button>
-Elements hat eine besondere Bedeutung für React, weil es eine native Komponente ist. Die Benennung eigener Komponenten, wie Square, ist dir überlassen. Wir könnten dieonClick
-Prop der Square-Komponente oder diehandleClick
-Methode der Board-Komponente auch anders benennen und der Code würde trotzdem funktionieren. In React ist es eine Konvention,on[Event]
-Namen für Props zu benutzen, die Events repräsentieren undhandle[Event]
für Methoden, die Events behandeln.
Wenn wir versuchen ein Quadrat anzuklicken, sollten wir einen Fehler bekommen, da wir handleClick
bisher noch nicht definiert haben. Wir fügen handleClick
nun zur Board-Komponente hinzu:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Schau dir den bis jetzt vorhandenen Code an
Nach diesen Änderungen sind wir wieder in der Lage auf die Quadrate zu klicken, um diese zu befüllen. Jedoch wird der State jetzt in der Board-Komponente anstatt in jeder einzelnen Square-Komponente gespeichert. Wenn sich der State der Board-Komponente ändert, werden automatisch die Square-Komponenten neu gerendert. Das Speichern des States aller Quadrate in der Board-Komponente gibt ihr die Möglichkeit, später den Gewinner zu bestimmen.
Dadurch, dass die Square-Komponenten nicht mehr ihren eigenen State verwalten, erhalten sie die Werte von der Board-Komponente. Wenn die Square-Komponenten geklickt wurden, informieren sie die Board-Komponente. Im React-Jargon werden die Square-Komponenten nun als kontrollierte Komponenten (engl. controlled components) bezeichnet. Die Board-Komponente hat volle Kontrolle über sie.
Beachte, wie wir in der handleClick
-Methode die .slice()
-Methode aufrufen, um eine neue Kopie des squares
-Arrays zu erzeugen, die wir bearbeiten, anstatt das bestehende Array zu bearbeiten. Im nächsten Abschnitt werden wir erklären, warum wir eine Kopie des squares
-Arrays erzeugen.
Warum Unveränderlichkeit (engl. Immutability) wichtig ist
Im vorherigen Codebeispiel haben wir vorgeschlagen, dass du eine Kopie des Arrays squares
mit der slice()
-Methode erstellst, anstatt das bestehende Array zu verändern. Wir werden nun die Unveränderlichkeit besprechen und warum es wichtig ist, diese zu lernen.
Prinzipiell gibt es zwei Ansätze, Daten zu verändern. Der erste Ansatz ist, die Daten abzuändern (engl. mutate) indem man die Werte der Daten direkt ändert. Der zweite Ansatz ist, die Daten durch eine neue Kopie zu ersetzen, welche die gewünschten Änderungen enthält.
Datenänderung durch direktes Abändern
var player = {score: 1, name: 'Jeff'};
player.score = 2;
// player ist jetzt {score: 2, name: 'Jeff'}
Ändern von Daten auf Grundlage einer Kopie bestehender Daten
var player = {score: 1, name: 'Jeff'};
var newPlayer = Object.assign({}, player, {score: 2});
// player ist unverändert, aber newPlayer ist jetzt {score: 2, name: 'Jeff'}
// Oder alternativ, wenn du die Object-Spread-Syntax verwendest:
// var newPlayer = {...player, score: 2};
Das Endergebnis ist das gleiche, aber dadurch, dass Daten nicht direkt abgeändert werden, gewinnen wir einige Vorteile, die nachfolgend beschrieben werden.
Komplexe Funktionalitäten werden einfach
Durch Unveränderlichkeit können komplexe Funktionalitäten einfacher implementiert werden. Später in diesem Tutorial werden wir eine “Zeitreise”-Funktionalität (engl. time travel) implementieren, die es uns erlaubt, Einsicht in die Tic-Tac-Toe-Spielhistorie zu gewinnen und zu vorherigen Zügen “zurückzuspringen”. Diese Funktionalität ist nicht nur für Spiele geeignet — die Möglichkeit bestimmte Aktionen rückgängig zu machen und zu wiederholen ist eine häufige Anforderung für Anwendungen. Indem wir das direkte Abändern von Daten vermeiden, können wir vorherige Versionen der Spielhistorie intakt lassen und diese zu einem späteren Zeitpunkt wiederverwenden.
Veränderungen erkennen
Veränderungen in änderbaren Objekten zu erkennen ist schwierig, da diese direkt angepasst werden. Diese Erkennung erfordert, dass das änderbare Objekt mit seinen vorherigen Kopien verglichen wird. Außerdem müsste der gesamte Objekt-Baum durchlaufen werden.
Veränderungen in unveränderbaren Objekten zu erkennen ist erheblich leichter. Falls das unveränderbare Objekt, auf das verwiesen wird, anders ist als das vorhergehende, dann hat sich das Objekt verändert.
Entscheiden, wann in React neu gerendert werden soll
Der Hauptvorteil von Unveränderlichkeit ist, dass es dir hilft pure components in React zu entwickeln. Anhand unveränderbarer Daten lässt sich leicht feststellen, ob Änderungen vorgenommen wurden. Dadurch lässt sich leichter ermitteln, wann eine Komponente neu gerendert werden muss.
Du kannst mehr über shouldComponentUpdate()
und das Entwickeln von pure components lernen, wenn du Performance-Optimierung liest.
Funktionskomponenten
Wir ändern nun die Square-Komponente in eine Funktionskomponente.
In React sind Funktionskomponenten ein leichterer Weg, um Komponenten zu schreiben,
welche nur eine render
-Methode beinhalten und keinen eigenen State haben.
Statt eine Klasse zu definieren, welche React.Component
erweitert, können wir eine Funktion schreiben, welche props
als Input nimmt und zurückgibt, was gerendert werden soll.
Funktionskomponenten sind weniger mühsam zu schreiben als Klassen und viele Komponenten können auf diesem Weg formuliert werden.
Ersetze die Square-Klasse mit dieser Funktion:
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
Wir haben beide vorkommenden this.props
mit props
ersetzt.
Schau dir den bis jetzt vorhandenen Code an
Hinweis
Als wir die Square-Komponente in eine Funktionskomponente geändert haben, haben wir auch
onClick={() => this.props.onClick()}
in ein kürzeresonClick={props.onClick}
geändert (beachte, dass die Klammern auf beiden Seiten fehlen).
Abwechselnd einen Zug machen
Nun müssen wir einen offensichtlichen Defekt in unserem Tic-Tac-Toe Spiel beheben: Die “O”s können nicht auf dem Spielfeld gesetzt werden.
Wir setzen den ersten Zug standardmäßig auf “X”. Das können wir erreichen, indem wir den initialen State in unserem Board-Konstruktor verändern:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
Jedes Mal wenn ein Spieler einen Zug macht, wird xIsNext
(ein Boolean) geändert, um den nächsten Spieler zu bestimmen. Außerdem wird der Stand des Spiels gespeichert.
Wir aktualisieren die handleClick
-Funktion der Board-Komponente, um den Wert von xIsNext
zu ändern:
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({
squares: squares,
xIsNext: !this.state.xIsNext, });
}
Durch diese Änderung wechseln sich “X”e und “O”s ab. Probiere es aus!
Lass uns ebenfalls den “status”-Text in der render
-Funktion der Board-Komponente ändern, sodass sie anzeigt, welcher Spieler als nächstes an der Reihe ist:
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
// Der Rest hat sich nicht geändert
Nachdem wir diese Änderungen angewandt haben, sollte deine Board-Komponente so aussehen:
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true, };
}
handleClick(i) {
const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); }
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
);
}
render() {
const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Schau dir den bis jetzt vorhandenen Code an
Einen Gewinner bekanntgeben
Da wir nun den nächsten Spieler anzeigen können, sollten wir ebenfalls anzeigen, wann das Spiel gewonnen ist und dass keine Züge mehr möglich sind. Kopiere diese Hilfsfunktion an das Ende der Datei:
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
Bei einem Array mit 9 Quadraten prüft diese Funktion, ob es einen Gewinner gibt, und gibt entsprechend 'X'
, 'O'
oder null
zurück.
Wir werden calculateWinner(squares)
in der render
-Funktion des Boards aufrufen, um zu prüfen, ob ein Spieler gewonnen hat. Falls ein Spieler gewonnen hat, können wir beispielsweise folgenden Text anzeigen: “Winner: X” or “Winner: O”. Wir ersetzen die status
-Deklaration in der render
-Funktion des Boards mit folgendem Code:
render() {
const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
// Der Rest hat sich nicht verändert
Wir können nun die handleClick
-Funktion der Board-Komponente so ändern, dass sie vorzeitig zurückkehrt, indem wir einen Klick ignorieren, wenn jemand das Spiel gewonnen hat oder wenn ein Feld bereits gefüllt ist:
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
Schau dir den bis jetzt vorhandenen Code an
Herzlichen Glückwunsch! Du hast nun ein funktionierendes Tic-Tac-Toe-Spiel. Außerdem hast du noch die Grundlagen von React gelernt. Demnach bist du wahrscheinlich der echte Gewinner hier.
Zeitreisen hinzufügen
Lass uns als abschließende Übung eine “Versionsgeschichte” hinzufügen, um zu älteren Zügen im Spiel zurückzukehren.
Speichern eines Verlaufs von Spielzügen
Hätten wir das squares
-Array direkt verändert, wäre die Implementierung einer “Zeitreise”-Funktionalität sehr schwierig.
Wir haben jedoch slice()
verwendet, um nach jedem Zug eine neue Kopie des squares
-Arrays zu erstellen und haben das Array als unveränderbar behandelt.
Dies wird es uns erlauben, jede ältere Version des squares
-Arrays zu speichern und zwischen den Zügen zu springen, welche schon stattgefunden haben.
Wir werden die früheren squares
-Arrays in einem anderen Array namens history
speichern. Das history
-Array repräsentiert alle Zustände des Spielfelds, vom ersten bis zum letzten Zug und hat folgende Form:
history = [
// Vor dem ersten Zug
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// Nach dem ersten Zug
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// Nach dem zweiten Zug
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
Nun müssen wir entscheiden, welche Komponente den history
-State beinhalten soll.
Den State nochmal hochziehen
Wir möchten, dass die oberste Komponente, Game, eine Liste der bisherigen Züge anzeigt.
Um dies zu tun, benötigt sie Zugriff auf history
.
Deshalb platzieren wir den history
-State in der obersten Komponente, Game.
Das Platzieren des history
-States in der Game-Komponente erlaubt es uns, den squares
-States von dessen Kindkomponente, Board, zu entfernen. Genauso wie wir den State von der Square-Komponente in die Board-Komponente “hochgezogen” haben, so ziehen wir diesen nun von der Board- in die übergeordnete Game-Komponente. Das gibt der Game-Komponente uneingeschränkte Kontrolle über die Daten der Board-Komponente und ermöglicht ihr, die Board-Komponente anzuweisen, vorherige Züge aus der history
zu rendern.
Als erstes setzen wir den initialen State für die Game-Komponente in deren Konstruktor:
class Game extends React.Component {
constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; }
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
Als nächstes erhält die Board-Komponente squares
- und onClick
-Props von der Game-Komponente.
Da wir nun in der Board-Komponente einen einzigen Klick-Handler für viele Squares haben, müssen wir die Position jedes Squares in den onClick
-Handler übergeben, um anzugeben, welches Square geklickt wurde. Hier sind die benötigten Schritte, um die Board-Komponente zu verändern:
- Lösche den
constructor
in der Board-Komponente. - Ersetze
this.state.squares[i]
mitthis.props.squares[i]
in derrenderSquare
-Funktion der Board-Komponente. - Ersetze
this.handleClick(i)
mitthis.props.onClick(i)
in derrenderSquare
-Funktion der Board-Komponente.
Die Board-Komponente sieht nun so aus:
class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
renderSquare(i) {
return (
<Square
value={this.props.squares[i]} onClick={() => this.props.onClick(i)} />
);
}
render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Wir erweitern die render
-Funktion der Game-Komponente, damit der letzte Eintrag der Versionshistorie zur Ermittlung und Anzeige des Spielstandes verwendet wird:
render() {
const history = this.state.history; const current = history[history.length - 1]; const winner = calculateWinner(current.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); }
return (
<div className="game">
<div className="game-board">
<Board squares={current.squares} onClick={(i) => this.handleClick(i)} /> </div>
<div className="game-info">
<div>{status}</div> <ol>{/* TODO */}</ol>
</div>
</div>
);
}
Da die Game-Komponente nun den Spielstand rendert, können wir den dazugehörigen Code aus der render
-Methode der Board-Komponente entfernen. Nach der Überarbeitung sollte die render
-Funktion der Board-Komponente so aussehen:
render() { return ( <div> <div className="board-row"> {this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
Als letztes müssen wir nun die handleClick
-Methode aus der Board-Komponente in die Game-Komponente verschieben.
Außerdem müssen wir handleClick
verändern, da der State der Game-Komponente anders strukturiert ist.
In der handleClick
-Methode der Game-Komponente fügen wir die neuen Einträge der Versionsgeschichte zur bestehenden history
hinzu.
handleClick(i) {
const history = this.state.history; const current = history[history.length - 1]; const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({ history: history.concat([{ squares: squares, }]),
xIsNext: !this.state.xIsNext,
});
}
Hinweis
Anders als die
push()
-Methode des Arrays, mit welcher du vielleicht vertrauter bist, verändert dieconcat()
-Methode das ursprüngliche Array nicht, weshalb wir diese bevorzugen.
An diesem Punkt benötigt die Board-Komponente nur noch die renderSquare
- und render
-Methode.
Der Spielstand und die handleClick
-Methode sollten sich in der Game-Komponente befinden.
Schau dir den bis jetzt vorhandenen Code an
Die letzten Züge anzeigen
Seit wir den Verlauf des Tic-Tac-Toe-Spiels aufzeichnen, können wir diesen nun dem Spieler als eine Liste der letzten Züge anzeigen.
Wir haben vorhin gelernt, dass React-Elemente first-class JavaScript-Objekte sind; wir können diese in unserer Anwendung hin- und herreichen. Um mehrere Elemente in React zu rendern, können wir ein Array bestehend aus React-Elementen verwenden.
Arrays haben in JavaScript eine map()
-Methode, welche üblicherweise verwendet wird, um Daten auf andere Daten abzubilden, wie zum Beispiel:
const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]
Durch die Verwendung der map
-Methode können wir unseren Verlauf von Spielzügen auf React-Elemente abbilden, welche Buttons auf dem Bildschirm repräsentieren. Des Weiteren können wir eine Liste von Buttons anzeigen, um zu vergangenen Zügen zu “springen”.
Lass uns in der render
-Methode der Game-Komponente über die history
mappen.
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol> </div>
</div>
);
}
Schau dir den bis jetzt vorhandenen Code an
Während wir über das history
-Array iterieren, verweist die step
-Variable auf den aktuellen Wert des history
-Elements und move
auf den Index des aktuellen history
-Elements. Wir sind hier nur an move
interessiert, deshalb wird step
nichts zugewiesen.
Für jeden Spielzug in der Historie des Tic-Tac-Toe-Spiels erzeugen wir ein Listenelement <li>
, welches einen Button <button>
enthält. Der Button hat einen onClick
-Handler, welcher eine Methode namens this.jumpTo()
aufruft. Wir haben die jumpTo()
-Methode bis jetzt noch nicht implementiert. Vorerst sollten wir eine Liste aller Spielzüge sehen, die während des Spiels getätigt wurden. Des Weiteren sollten wir in der Konsole des Entwicklerwerkzeugs folgende Warnung sehen:
Warnung: Jedes Kind in einem Array oder Iterator sollte eine eindeutige “key”-Prop haben. Überprüfe die render-Methode von “Game”.
Lass uns herausfinden, was diese Warnung bedeutet.
Einen Schlüssel (key) auswählen
Wenn wir eine Liste rendern, speichert React einige Informationen über jedes gerenderte Listenelement. Wenn wir eine Liste aktualisieren, muss React herausfinden, was sich geändert hat. Wir könnten Elemente der Liste hinzugefügt, entfernt, neu angeordnet oder aktualisiert haben.
Stell dir eine Veränderung von
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
zu
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
Zusätzlich zu den aktualisierten Zahlen, könnte ein Mensch, der das liest, wahrscheinlich erkennen, dass wir die Reihenfolge von Alexa und Ben vertauscht und wir Claudia zwischen Alexa und Ben eingefügt haben. React ist jedoch ein Computerprogramm und kennt unsere Absichten nicht. Da React unsere Absichten nicht kennen kann, müssen wir eine key-Eigenschaft für jedes Listenelement spezifizieren, um es von seinen Geschwistern unterscheiden zu können. Eine Option wäre, die Strings alexa
, ben
und claudia
zu verwenden. Falls wir Daten aus einer Datenbank darstellen würden, könnten die Datenbank-IDs von Alexa, Ben und Claudia als Schlüssel verwendet werden.
<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>
Wenn eine Liste neu gerendert wird, nimmt React den Schlüssel von jedem Element der Liste und durchsucht die Elemente der vorherigen Liste nach einem passenden Schlüssel. Falls die aktuelle Liste einen Schlüssel enthält, welcher vorher nicht existiert hat, erstellt React eine Komponente. Falls die in der aktuellen Liste ein Schlüssel fehlt, welcher in der vorherigen Liste existierte, zerstört React die vorherige Komponente. Falls zwei Schlüssel übereinstimmen, wird die entsprechende Komponente verschoben. Schlüssel teilen React die Identität jeder Komponente mit. Dies ermöglicht es React, den Zustand zwischen den einzelnen Renderings beizubehalten. Falls sich der Schlüssel einer Komponente ändert, wird die Komponente zerstört und mit einem neuen Zustand neu erzeugt.
key
ist eine spezielle und reservierte Eigenschaft in React (zusammen mit ref
, einer erweiterten Funktion). Wenn ein Element erzeugt wird, extrahiert React die key
-Eigenschaft und speichert den Schlüssel direkt im zurückgegebenen Element. Auch wenn key
so aussieht, als gehöre es in props
, kann key
nicht mit this.props.key
referenziert werden. React verwendet automatisch key
, um zu entscheiden, welche Komponenten aktualisiert werden sollen. Eine Komponente kann nicht nach ihrem key
fragen.
Es wird sehr empfohlen, dass du vernünftige Schlüssel zuweist, immer wenn du dynamische Listen baust. Falls du keinen passenden Schlüssel hast, könntest du in Betracht ziehen deine Daten so zu restrukturieren, dass du einen solchen Schlüssel hast.
Wenn kein Schlüssel spezifiziert ist, wird React eine Warnung anzeigen und den Index des Arrays standardmäßig als Schlüssel verwenden. Die Verwendung des Array-Index als Schlüssel ist problematisch, wenn man versucht, Elemente einer Liste neu anzuordnen oder Listenelemente hinzuzufügen oder zu entfernen. Die explizite Übergabe von key={i}
schaltet die Warnung aus, es bestehen aber die gleichen Probleme wie bei Array-Indizies. Deshalb wird es in den meisten Fällen nicht empfohlen.
Schlüssel müssen nicht global eindeutig sein; sie müssen nur zwischen Komponenten und deren Geschwistern eindeutig sein.
Zeitreisen implementieren
In der Historie des Tic-Tac-Toe-Spiels ist jedem vergangenen Zug eine eindeutige ID zugeordnet: die fortlaufende Nummer des Zuges. Die Züge werden nie neu geordnet, gelöscht oder in der Mitte eingefügt; es ist also sicher, den Index des Spielzugs als Schlüssel zu verwenden.
In der render
-Methode der Game-Komponente können wir den Schlüssel als <li key={move}>
hinzufügen und Reacts Warnung bezüglich der Schlüssel sollte verschwinden:
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
Schau dir den bis jetzt vorhandenen Code an
Wenn man auf einen der Buttons der Listenelemente klickt, wird ein Fehler geworfen, da die jumpTo
-Methode nicht definiert ist. Bevor wir jumpTo
implementieren, fügen wir stepNumber
zum State der Game-Komponente hinzu, um anzuzeigen, welchen Schritt wir aktuell sehen.
Füge als erstes stepNumber: 0
zum initialen State im constructor
der Game-Komponente hinzu:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0, xIsNext: true,
};
}
Als nächstes definieren wir die jumpTo
-Methode in der Game-Komponente, um stepNumber
zu aktualiseren. Außerdem setzen wir xIsNext
auf true, wenn die Nummer, in die wir stepNumber
ändern, gerade ist:
handleClick(i) {
// Diese Methode hat sich nicht geändert
}
jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); }
render() {
// Diese Methode hat sich nicht geändert
}
Beachte, dass wir in der jumpTo
-Methode die history
-Eigenschaft des States nicht aktualisert haben. Das ist deshalb so, weil State-Aktualisierungen zusammengeführt werden. In einfacheren Worten: React wird nur die Eigenschaften aktualisieren, die in der setState
-Methode erwähnt werden und lässt den restlichen State unverändert.
Schau dir für weitere Informationen die Dokumentation an.
Wir werden jetzt einige Änderungen an der handleClick
-Methode der Game-Komponente vornehmen. Diese wird aufgerufen, wenn du auf ein Quadrat klickst.
Der State stepNumber
, den wir hinzugefügt haben, stellt jetzt den Spielzug dar, der dem Benutzer angezeigt wird. Nachdem wir einen neuen Spielzug gemacht haben, müssen wir stepNumber
aktualisieren, indem wir stepNumber: history.length
als Teil des this.setState
-Arguments hinzufügen. Das stellt sicher, dass wir nicht ständig den gleichen Spielzug anzeigen, nachdem ein neuer gemacht wurde.
Außerdem werden wir this.state.history
mit this.state.history.slice(0, this.state.stepNumber + 1)
ersetzen. Das stellt sicher, dass wir, wenn wir “in der Zeit zurückgehen” und von diesem Punkt ausgehend einen neuen Spielzug machen, den gesamten “zukünftigen” Spielverlauf verwerfen, welcher nun nicht mehr stimmen würde.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length, xIsNext: !this.state.xIsNext,
});
}
Abschließend werden wir die render
-Methode der Game-Komponente so ändern, dass sie anstatt immer den letzten Spielzug zu rendern, den aktuell ausgewählten Spielzug entsprechend der stepNumber
rendert:
render() {
const history = this.state.history;
const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares);
// Der Rest hat sich nicht geändert
Wenn wir auf einen beliebigen Schritt im Spielverlauf klicken, sollte das Tic-Tac-Toe-Spielbrett sofort aktualisiert werden und zeigen, wie das Spielbrett nach diesem Schritt aussah.
Schau dir den bis jetzt vorhandenen Code an
Zusammenfassung
Glückwunsch! Du hast ein Tic-Tac-Toe-Spiel entwickelt, welches:
- dir erlaubt Tic-Tac-Toe zu spielen,
- angibt, wenn ein Spieler das Spiel gewonnen hat,
- den Spielverlauf speichert, während das Spiel noch läuft,
- den Spielern erlaubt, einen Spielverlauf erneut anzusehen und vorherige Versionen eines Spielbretts anzuschauen.
Gute Arbeit! Wir hoffen, dass du nun das Gefühl hast, die Funktionsweise von React gut zu verstehen.
Sieh dir das Endergebnis hier an: Endergebnis.
Falls du noch Zeit hast oder an deinen neuen React-Fähigkeiten arbeiten möchtest, findest du hier einige Ideen für Verbesserungen, welche du in das Tic-Tac-Toe-Spiel einbauen könntest. Die Elemente sind nach aufsteigendem Schwierigkeitsgrad sortiert.
- Zeige die Position für jeden Zug im Format (col, row) in der Liste des Spielverlaufs an.
- Formatiere das ausgewählte Element in der Zugliste fett.
- Schreibe Board so um, dass du zwei Schleifen verwendest um die Quadrate zu erzeugen, anstatt sie fest in den Quelltext zu schreiben.
- Füge einen Umschalt-Button hinzu, welcher dir erlaubt die Züge in aufsteigender oder absteigender Reihenfolge zu sortieren.
- Falls jemand gewinnt, markiere die 3 Quadrate, welche zum Gewinn führten.
- Falls keiner gewinnt, zeige eine Nachricht an, dass das Spiel unentschieden ist.
Während dieses Tutorials haben wir die grundlegenden Konzepte von React betrachtet, inklusive Elemente, Komponenten, Props und State.
Eine detailliertere Erklärung zu jedem dieser Themen findest du im Rest der Dokumentation. Um mehr darüber zu lernen, wie man Komponenten definiert, lohnt sich ein Blick in die React.Component
API-Referenz.