Tic-Tac-Toe
In diesem Tutorial wirst du ein kleines Tic-Tac-Toe-Spiel erstellen. Dieses Tutorial setzt keine Vorkenntnisse in React voraus. Die Techniken, die du im Tutorial lernst, sind grundlegend für den Bau jeder React-App, und ein vollständiges Verständnis wird dir ein tiefes Verständnis von React vermitteln.
Hinweis
Dieses Tutorial ist für Personen gedacht, die lieberlearning by doinglernen und schnell etwas Greifbares ausprobieren möchten. Wenn du es vorziehst, jedes Konzept Schritt für Schritt zu lernen, beginne mitDescribing the UI.
Das Tutorial ist in mehrere Abschnitte unterteilt:
- Setup für das Tutorialgibt direinen Ausgangspunkt, um dem Tutorial zu folgen.
- Überblicklehrt dichdie Grundlagenvon React: Komponenten, Props und State.
- Vervollständigung des Spielslehrt dichdie gängigsten Technikenin der React-Entwicklung.
- Zeitreise hinzufügengibt direinen tieferen Einblickin die einzigartigen Stärken von React.
Was wirst du bauen?
In diesem Tutorial wirst du ein interaktives Tic-Tac-Toe-Spiel mit React bauen.
Du kannst hier sehen, wie es aussehen wird, wenn du fertig bist:
Wenn der Code für dich noch keinen Sinn ergibt oder du mit der Syntax des Codes nicht vertraut bist, keine Sorge! Das Ziel dieses Tutorials ist es, dir zu helfen, React und seine Syntax zu verstehen.
Wir empfehlen, dass du dir das oben gezeigte Tic-Tac-Toe-Spiel ansiehst, bevor du mit dem Tutorial fortfährst. Eine der Funktionen, die dir auffallen wird, ist die nummerierte Liste rechts neben dem Spielbrett. Diese Liste gibt dir einen Verlauf aller Züge, die im Spiel gemacht wurden, und wird während des Spiels aktualisiert.
Sobald du das fertige Tic-Tac-Toe-Spiel ausprobiert hast, scrolle weiter. Du beginnst in diesem Tutorial mit einer einfacheren Vorlage. Unser nächster Schritt ist es, dich einzurichten, damit du mit dem Bau des Spiels beginnen kannst.
Setup für das Tutorial
Klicke im Live-Code-Editor unten aufForkin der oberen rechten Ecke, um den Editor in einem neuen Tab über die Website CodeSandbox zu öffnen. CodeSandbox ermöglicht es dir, Code in deinem Browser zu schreiben und eine Vorschau davon zu sehen, wie deine Benutzer die von dir erstellte App sehen werden. Der neue Tab sollte ein leeres Quadrat und den Startcode für dieses Tutorial anzeigen.
Hinweis
Du kannst diesem Tutorial auch mit deiner lokalen Entwicklungsumgebung folgen. Dazu musst du:
- InstalliereNode.js
- Drücke im zuvor geöffneten CodeSandbox-Tab die Schaltfläche in der oberen linken Ecke, um das Menü zu öffnen, und wähle dannDownload Sandboxin diesem Menü, um ein Archiv der Dateien lokal herunterzuladen
- Entpacke das Archiv, öffne dann ein Terminal und
cdin das entpackte Verzeichnis - Installiere die Abhängigkeiten mit
npm install - Führe
npm startaus, um einen lokalen Server zu starten, und folge den Anweisungen, um den Code in einem Browser laufen zu sehen
Wenn du stecken bleibst, lass dich davon nicht aufhalten! Folge dem Tutorial stattdessen online und versuche das lokale Setup später noch einmal.
Überblick
Jetzt, da du eingerichtet bist, lass uns einen Überblick über React bekommen!
Untersuchen des Startcodes
In CodeSandbox siehst du drei Hauptbereiche:

- DerDateien-Abschnitt mit einer Liste von Dateien wie
App.js,index.js,styles.cssimsrc-Ordner und einem Ordner namenspublic - DerCode-Editor, in dem Sie den Quellcode Ihrer ausgewählten Datei sehen
- DerBrowser-Abschnitt, in dem Sie sehen, wie der geschriebene Code angezeigt wird
Die DateiApp.js sollte im Dateien-Abschnitt ausgewählt sein. Der Inhalt dieser Datei imCode-Editorsollte sein:
DerBrowser-Abschnitt sollte ein Quadrat mit einem X darin anzeigen, etwa so:

Werfen wir nun einen Blick auf die Dateien im Startcode.
App.js
Der Code inApp.jserstellt eineKomponente. In React ist eine Komponente ein wiederverwendbares Code-Stück, das einen Teil einer Benutzeroberfläche repräsentiert. Komponenten werden verwendet, um UI-Elemente in Ihrer Anwendung zu rendern, zu verwalten und zu aktualisieren. Sehen wir uns die Komponente Zeile für Zeile an, um zu verstehen, was passiert:
Die erste Zeile definiert eine Funktion namensSquare. Das JavaScript-Schlüsselwortexportmacht diese Funktion außerhalb dieser Datei zugänglich. Das Schlüsselwortdefaultteilt anderen Dateien, die Ihren Code verwenden, mit, dass es sich um die Hauptfunktion in Ihrer Datei handelt.
Die zweite Zeile gibt einen Button zurück. Das JavaScript-Schlüsselwortreturnbedeutet, dass alles, was danach kommt, als Wert an den Aufrufer der Funktion zurückgegeben wird.<button>ist einJSX-Element. Ein JSX-Element ist eine Kombination aus JavaScript-Code und HTML-Tags, die beschreibt, was angezeigt werden soll.className="square"ist eine Button-Eigenschaft oder einProp, das CSS mitteilt, wie der Button gestylt werden soll.Xist der Text, der innerhalb des Buttons angezeigt wird, und</button>schließt das JSX-Element, um anzuzeigen, dass nachfolgende Inhalte nicht innerhalb des Buttons platziert werden sollen.
styles.css
Klicken Sie imstyles.cssDie ersten beidenDateien-Abschnitt von CodeSandbox auf die Datei mit der BezeichnungCSS-Selektoren(*undbody) definieren den Stil großer Teile Ihrer App, während der Selektor.squareden Stil jeder Komponente definiert, bei der die EigenschaftclassName auf squaregesetzt ist. In Ihrem Code würde das auf den Button Ihrer Square-Komponente in der DateiApp.jspassen.
index.js
Klicken Sie imindex.js. Sie werden diese Datei während des Tutorials nicht bearbeiten, aber sie ist die Brücke zwischen der Komponente, die Sie in der DateiDateien-Abschnitt von CodeSandbox auf die Datei mit der BezeichnungApp.jserstellt haben, und dem Webbrowser.
Die Zeilen 1-5 bringen alle notwendigen Teile zusammen:
- React
- React-Bibliothek zur Kommunikation mit Webbrowsern (React DOM)
- die Stile für Ihre Komponenten
- die Komponente, die Sie in
App.jserstellt haben.
Der Rest der Datei bringt alle Teile zusammen und injiziert das Endprodukt inindex.htmlim Ordnerpublic.
Das Spielfeld erstellen
Kehren wir zuApp.jszurück. Hier werden Sie den Rest des Tutorials verbringen.
Derzeit besteht das Spielfeld nur aus einem einzigen Feld, aber Sie benötigen neun! Wenn Sie einfach versuchen, Ihr Feld zu kopieren und einzufügen, um zwei Felder wie dieses zu erstellen:
erhalten Sie diesen Fehler:
Konsole
/src/App.js: Benachbarte JSX-Elemente müssen von einem umschließenden Tag eingeschlossen werden. Wollten Sie einen JSX-Fragment<>...</>verwenden?
React-Komponenten müssen ein einzelnes JSX-Element zurückgeben und nicht mehrere benachbarte JSX-Elemente wie zwei Buttons. Um dies zu beheben, können SieFragmente(<>und</>) verwenden, um mehrere benachbarte JSX-Elemente wie folgt zu umschließen:
Jetzt sollten Sie Folgendes sehen:

Großartig! Jetzt müssen Sie nur noch ein paar Mal kopieren und einfügen, um neun Felder hinzuzufügen und…

Oh nein! Die Felder sind alle in einer einzigen Reihe, nicht in einem Raster, wie Sie es für unser Spielfeld benötigen. Um dies zu beheben, müssen Sie Ihre Felder mitdivs in Reihen gruppieren und einige CSS-Klassen hinzufügen. Während Sie dabei sind, geben Sie jedem Feld eine Nummer, um sicherzustellen, dass Sie wissen, wo jedes Feld angezeigt wird.
Aktualisieren Sie in der DateiApp.js die Square-Komponente so, dass sie wie folgt aussieht:
Das instyles.cssdefinierte CSS gestaltet die divs mit derclassName board-row. Nachdem Sie Ihre Komponenten nun mit den gestaltetendivs in Reihen gruppiert haben, haben Sie Ihr Tic-Tac-Toe-Spielfeld:

Aber jetzt haben Sie ein Problem. Ihre Komponente namensSquareist eigentlich kein Feld mehr. Beheben wir das, indem wir den Namen inBoardändern:
An diesem Punkt sollte Ihr Code in etwa so aussehen:
Hinweis
Psssst… Das ist eine Menge zu tippen! Es ist in Ordnung, Code von dieser Seite zu kopieren und einzufügen. Wenn Sie jedoch eine kleine Herausforderung suchen, empfehlen wir, nur Code zu kopieren, den Sie mindestens einmal selbst manuell getippt haben.
Daten über Props übergeben
Als Nächstes möchten Sie den Wert eines Feldes von leer auf „X“ ändern, wenn der Benutzer auf das Feld klickt. Mit der bisherigen Art, wie Sie das Spielfeld aufgebaut haben, müssten Sie den Code, der das Feld aktualisiert, neunmal kopieren und einfügen (einmal für jedes Feld, das Sie haben)! Statt zu kopieren und einzufügen, ermöglicht Ihnen Reacts Komponentenarchitektur, eine wiederverwendbare Komponente zu erstellen, um unübersichtlichen, duplizierten Code zu vermeiden.
Zuerst kopieren Sie die Zeile, die Ihr erstes Feld definiert (<button className="square">1</button>) aus IhrerBoard-Komponente in eine neueSquare-Komponente:
Dann aktualisieren Sie die Board-Komponente, um dieseSquare-Komponente mit JSX-Syntax zu rendern:
Beachten Sie, dass Ihre eigenen Komponentendivs – mit einem Großbuchstaben beginnen müssen.BoardundSquare– anders als die Browser-
Sehen wir uns das an:

Oh nein! Sie haben die nummerierten Felder verloren, die Sie vorher hatten. Jetzt steht in jedem Feld „1“. Um dies zu beheben, verwenden SieProps, um den Wert, den jedes Feld haben soll, von der übergeordneten Komponente (Board) an ihre Kindkomponente (Square) zu übergeben.
Aktualisieren Sie dieSquare-Komponente, um dievalue-Prop zu lesen, die Sie vonBoardübergeben werden:
function Square({ value })zeigt an, dass der Square-Komponente eine Prop namensvalueübergeben werden kann.
Jetzt möchten Sie diesenvalueanstelle von1in jedem Feld anzeigen. Versuchen Sie es so:
Hoppla, das ist nicht das, was Sie wollten:

Sie wollten die JavaScript-Variable namensvalueaus Ihrer Komponente rendern, nicht das Wort „value“. Um von JSX „in JavaScript zu entkommen“, benötigen Sie geschweifte Klammern. Fügen Sie geschweifte Klammern umvaluein JSX hinzu, etwa so:
Im Moment sollten Sie ein leeres Brett sehen:

Das liegt daran, dass dieBoard-Komponente dievalue-Prop noch nicht an jedeSquare-Komponente übergeben hat, die sie rendert. Um dies zu beheben, fügen Sie dievalue-Prop zu jederSquare-Komponente hinzu, die von derBoard-Komponente gerendert wird:
Jetzt sollten Sie wieder ein Gitter mit Zahlen sehen:

Ihr aktualisierter Code sollte so aussehen:
Eine interaktive Komponente erstellen
Lassen Sie uns dieSquare-Komponente mit einemXfüllen, wenn Sie darauf klicken. Deklarieren Sie eine Funktion namenshandleClickinnerhalb vonSquare. Fügen Sie dannonClickzu den Props des Button-JSX-Elements hinzu, das vonSquarezurückgegeben wird:
Wenn Sie jetzt auf ein Quadrat klicken, sollten Sie einen Log-Eintrag mit"clicked!"in derKonsolesehen, die sich imBrowser-Bereich von CodeSandbox unten befindet. Wenn Sie mehrfach auf das Quadrat klicken, wird"clicked!"erneut geloggt. Wiederholte Konsolenausgaben mit derselben Nachricht erzeugen keine neuen Zeilen in der Konsole. Stattdessen sehen Sie einen Zähler neben Ihrem ersten"clicked!"-Log, der hochgezählt wird.
Hinweis
Wenn Sie dieses Tutorial in Ihrer lokalen Entwicklungsumgebung verfolgen, müssen Sie die Konsole Ihres Browsers öffnen. Wenn Sie beispielsweise den Chrome-Browser verwenden, können Sie die Konsole mit der TastenkombinationUmschalt + Strg + J(unter Windows/Linux) oderWahl + ⌘ + J(unter macOS) aufrufen.
Als nächsten Schritt soll die Square-Komponente sich „merken“, dass sie angeklickt wurde, und sie mit einem „X“ füllen. Um sich Dinge zu „merken“, verwenden KomponentenState.
React stellt eine spezielle Funktion namensuseStatebereit, die Sie von Ihrer Komponente aus aufrufen können, um ihr das „Merken“ von Dingen zu ermöglichen. Speichern wir den aktuellen Wert desSquareim State und ändern ihn, wenn derSquareangeklickt wird.
Importieren SieuseStateam Anfang der Datei. Entfernen Sie dievalue-Prop aus derSquare-Komponente. Fügen Sie stattdessen am Anfang vonSquareeine neue Zeile ein, dieuseStateaufruft. Lassen Sie sie eine State-Variable namensvaluezurückgeben:
valuespeichert den Wert undsetValueist eine Funktion, mit der der Wert geändert werden kann. Das anuseStateübergebenenullwird als Anfangswert für diese State-Variable verwendet, daher istvaluehier zunächst gleichnull.
Da dieSquare-Komponente keine Props mehr akzeptiert, entfernen Sie dievalue-Prop aus allen neun von der Board-Komponente erstellten Square-Komponenten:
Jetzt ändern SieSquareso, dass bei einem Klick ein „X“ angezeigt wird. Ersetzen Sie den Event-Handlerconsole.log("clicked!"); durch setValue('X');. IhreSquare-Komponente sieht dann so aus:
Indem Sie dieseset-Funktion aus einemonClick-Handler aufrufen, weisen Sie React an, diesenSquareneu zu rendern, wenn sein<button>angeklickt wird. Nach dem Update ist derSquare-valuegleich'X', sodass Sie das „X“ auf dem Spielbrett sehen. Klicken Sie auf ein beliebiges Quadrat, und „X“ sollte erscheinen:

Jedes Quadrat hat seinen eigenen State: Der in jedem Quadrat gespeichertevalueist völlig unabhängig von den anderen. Wenn Sie eineset-Funktion in einer Komponente aufrufen, aktualisiert React automatisch auch die darin enthaltenen Kindkomponenten.
Nachdem Sie die oben genannten Änderungen vorgenommen haben, sieht Ihr Code wie folgt aus:
React Developer Tools
Mit React DevTools können Sie die Props und den State Ihrer React-Komponenten überprüfen. Den Tab für React DevTools finden Sie im unteren Bereich desBrowser-Abschnitts in CodeSandbox:

Um eine bestimmte Komponente auf dem Bildschirm zu inspizieren, verwenden Sie die Schaltfläche in der oberen linken Ecke der React DevTools:

Das Spiel vervollständigen
An diesem Punkt haben Sie alle grundlegenden Bausteine für Ihr Tic-Tac-Toe-Spiel. Um ein vollständiges Spiel zu haben, müssen Sie nun abwechselnd „X“ und „O“ auf dem Brett platzieren, und Sie benötigen eine Möglichkeit, einen Gewinner zu ermitteln.
State nach oben heben
Derzeit verwaltet jedeSquare-Komponente einen Teil des Spielzustands. Um einen Gewinner in einem Tic-Tac-Toe-Spiel zu überprüfen, müsste dieBoard-Komponente irgendwie den Zustand jeder der 9Square-Komponenten kennen.
Wie würden Sie das angehen? Zunächst könnten Sie vermuten, dass dasBoard jedes Squarenach dessen Zustand „fragen“ mussBoard-Komponente zu speichern und nicht in jedem einzelnen Square. Die Board-Komponente kann jedemSquaremitteilen, was es anzeigen soll, indem sie eine Prop übergibt, so wie Sie es getan haben, als Sie jedem Square eine Zahl übergeben haben.
Um Daten von mehreren Kindern zu sammeln oder um zwei Kindkomponenten miteinander kommunizieren zu lassen, deklarieren Sie den gemeinsamen State stattdessen in ihrer übergeordneten Komponente. Die übergeordnete Komponente kann diesen State dann über Props an die Kinder zurückgeben. Dies hält die Kindkomponenten miteinander und mit ihrer übergeordneten Komponente synchron.
Das Heben des States in eine übergeordnete Komponente ist üblich, wenn React-Komponenten refaktorisiert werden.
Nutzen wir diese Gelegenheit, um es auszuprobieren. Bearbeiten Sie dieBoard-Komponente so, dass sie eine State-Variable namenssquaresdeklariert, die standardmäßig ein Array mit 9 null-Werten ist, die den 9 Feldern entsprechen:
Array(9).fill(null)erstellt ein Array mit neun Elementen und setzt jedes davon aufnull. DeruseState()-Aufruf darum deklariert eine State-Variablesquares, die anfänglich auf dieses Array gesetzt wird. Jeder Eintrag im Array entspricht dem Wert eines Feldes. Wenn Sie das Brett später füllen, wird dassquares-Array so aussehen:
Jetzt muss IhreBoard-Komponente dievalue-Prop an jedesSquareweitergeben, das sie rendert:
Als nächstes bearbeiten Sie dieSquare-Komponente, damit sie dievalue-Prop von der Board-Komponente empfängt. Dies erfordert, dass die eigene State-verwaltende Verfolgung vonvalueund dieonClick-Prop der Schaltfläche in der Square-Komponente entfernt werden:
An diesem Punkt sollten Sie ein leeres Tic-Tac-Toe-Spielfeld sehen:

Und Ihr Code sollte so aussehen:
Jedes Quadrat erhält nun einevalue-Prop, die entweder'X','O'odernullfür leere Quadrate ist.
Als Nächstes müssen Sie ändern, was passiert, wenn einSquareangeklickt wird. DieBoard-Komponente verwaltet nun, welche Quadrate gefüllt sind. Sie müssen einen Weg schaffen, damit dasSquareden State desBoardaktualisieren kann. Da State privat für die Komponente ist, die ihn definiert, können Sie den State desBoardnicht direkt vomSquareaus aktualisieren.
Stattdessen übergeben Sie eine Funktion von derBoard-Komponente an dieSquare-Komponente, und dasSquareruft diese Funktion auf, wenn ein Quadrat angeklickt wird. Sie beginnen mit der Funktion, die dieSquare-Komponente bei einem Klick aufrufen wird. Sie nennen diese FunktiononSquareClick:
Als Nächstes fügen Sie dieonSquareClick-Funktion zu den Props derSquare-Komponente hinzu:
Jetzt verbinden Sie dieonSquareClick-Prop mit einer Funktion in derBoard-Komponente, die SiehandleClicknennen. UmonSquareClickmithandleClickzu verbinden, übergeben Sie eine Funktion an dieonSquareClick-Prop der erstenSquare-Komponente:
Schließlich definieren Sie diehandleClick-Funktion innerhalb der Board-Komponente, um dassquares-Array zu aktualisieren, das den State Ihres Spielfelds hält:
DiehandleClick-Funktion erstellt eine Kopie dessquares-Arrays (nextSquares) mit der JavaScript-Array-Methodeslice(). Dann aktualisierthandleClick das nextSquares-Array, umXzum ersten Quadrat (Index[0]) hinzuzufügen.
Der Aufruf dersetSquares-Funktion teilt React mit, dass sich der State der Komponente geändert hat. Dies löst ein erneutes Rendern der Komponenten aus, die densquares-State verwenden (Board), sowie deren Kindkomponenten (dieSquare-Komponenten, aus denen das Spielfeld besteht).
Hinweis
JavaScript unterstütztClosures, was bedeutet, dass eine innere Funktion (z.B.handleClick) Zugriff auf Variablen und Funktionen hat, die in einer äußeren Funktion (z.B.BoardDiehandleClick-Funktion kann densquares-State lesen und diesetSquares-Methode aufrufen, da beide innerhalb derBoard-Funktion definiert sind.
Jetzt können Sie X auf das Spielfeld setzen… aber nur im oberen linken Quadrat. IhrehandleClick-Funktion ist hartkodiert, um den Index für das obere linke Quadrat (0) zu aktualisieren. Lassen Sie unshandleClickso aktualisieren, dass es jedes Quadrat aktualisieren kann. Fügen Sie ein ArgumentizurhandleClick-Funktion hinzu, das den Index des zu aktualisierenden Quadrats annimmt:
Als Nächstes müssen Sie diesesianhandleClickübergeben. Sie könnten versuchen, die ProponSquareClickdes Quadrats direkt im JSX aufhandleClick(0)zu setzen, aber das wird nicht funktionieren:
Hier ist der Grund, warum das nicht funktioniert. Der AufrufhandleClick(0)wird Teil des Renderns der Board-Komponente sein. DahandleClick(0)den Zustand der Board-Komponente durch den Aufruf vonsetSquaresändert, wird Ihre gesamte Board-Komponente erneut gerendert. Dies führt jedoch wieder zuhandleClick(0), was eine Endlosschleife verursacht:
Konsole
Zu viele Neu-Render. React begrenzt die Anzahl der Render, um eine Endlosschleife zu verhindern.
Warum ist dieses Problem früher nicht aufgetreten?
Als SieonSquareClick={handleClick}übergeben haben, haben Sie die FunktionhandleClickals Prop weitergegeben. Sie haben sie nicht aufgerufen! Jetzt rufen Sie die Funktion jedochsofort auf– beachten Sie die Klammern inhandleClick(0)– und deshalb läuft sie zu früh ab. Sie möchtennicht, dasshandleClickaufgerufen wird, bevor der Benutzer klickt!
Sie könnten dies beheben, indem Sie eine Funktion wiehandleFirstSquareClickerstellen, diehandleClick(0)aufruft, eine Funktion wiehandleSecondSquareClick, diehandleClick(1)aufruft, und so weiter. Sie würden diese Funktionen (anstatt sie aufzurufen) als Props wieonSquareClick={handleFirstSquareClick}weitergeben. Dies würde die Endlosschleife lösen.
Allerdings ist es zu umständlich, neun verschiedene Funktionen zu definieren und jeder einen Namen zu geben. Stattdessen machen wir Folgendes:
Beachten Sie die neue Syntax() =>. Hier ist() => handleClick(0)eineArrow-Funktion, eine kürzere Art, Funktionen zu definieren. Wenn auf das Quadrat geklickt wird, wird der Code nach dem=>-„Pfeil“ ausgeführt, derhandleClick(0)aufruft.
Jetzt müssen Sie die anderen acht Quadrate aktualisieren, umhandleClickaus den Arrow-Funktionen aufzurufen, die Sie übergeben. Stellen Sie sicher, dass das Argument für jeden Aufruf vonhandleClickdem Index des richtigen Quadrats entspricht:
Jetzt können Sie wieder X in jedes Quadrat auf dem Brett setzen, indem Sie darauf klicken:

Aber dieses Mal wird die gesamte Zustandsverwaltung von derBoard-Komponente übernommen!
So sollte Ihr Code aussehen:
Da sich Ihre Zustandsverwaltung nun in derBoard-Komponente befindet, übergibt die übergeordneteBoard-Komponente Props an die untergeordnetenSquare-Komponenten, damit diese korrekt angezeigt werden können. Wenn auf einenSquaregeklickt wird, fordert die untergeordneteSquare-Komponente nun die übergeordneteBoard-Komponente auf, den Zustand des Bretts zu aktualisieren. Wenn sich der Zustand desBoardändert, werden sowohl dieBoard-Komponente als auch jede untergeordneteSquare-Komponente automatisch neu gerendert. Das Behalten des Zustands aller Quadrate in derBoard-Komponente ermöglicht es ihr, in Zukunft den Gewinner zu bestimmen.
Lassen Sie uns rekapitulieren, was passiert, wenn ein Benutzer auf das obere linke Quadrat Ihres Bretts klickt, um einXhinzuzufügen:
- Ein Klick auf das obere linke Feld führt die Funktion aus, die der
buttonals seineonClick-Prop von derSquareDieSquare-Komponente hat diese Funktion als ihreonSquareClick-Prop von derBoardDieBoard-Komponente hat diese Funktion direkt im JSX definiert. Sie rufthandleClickmit dem Argument0auf. handleClickverwendet das Argument (0), um das erste Element dessquares-Arrays vonnullaufXzu aktualisieren.- Der
squares-State derBoard-Komponente wurde aktualisiert, daher rendern dieBoard-Komponente und alle ihre Kinder neu. Dadurch ändert sich dievalue-Prop derSquare-Komponente mit dem Index0vonnullzuX.
Am Ende sieht der Benutzer, dass das obere linke Feld nach einem Klick von leer zu einem Feld mit einemXgeworden ist.
Hinweis
Das DOM-<button>Für benutzerdefinierte Komponenten wieSquare liegt die Namensgebung bei Ihnen. Sie könnten der onSquareClick-Prop vonSquareoder derhandleClick-Funktion vonBoardjeden beliebigen Namen geben, und der Code würde genauso funktionieren. In React ist es üblich, für Props, die Ereignisse repräsentieren, Namen mitonSomethingzu verwenden und für die Funktionsdefinitionen, die diese Ereignisse behandeln, Namen mithandleSomething.
Warum Unveränderlichkeit wichtig ist
Beachten Sie, wie Sie inhandleClick .slice()aufrufen, um eine Kopie dessquares-Arrays zu erstellen, anstatt das bestehende Array zu verändern. Um zu erklären warum, müssen wir über Unveränderlichkeit sprechen und warum es wichtig ist, sie zu verstehen.
Im Allgemeinen gibt es zwei Ansätze, um Daten zu ändern. Der erste Ansatz ist, die Daten zumutieren, indem die Werte der Daten direkt geändert werden. Der zweite Ansatz ist, die Daten durch eine neue Kopie zu ersetzen, die die gewünschten Änderungen enthält. So würde es aussehen, wenn Sie dassquares-Array mutieren würden:
Und so würde es aussehen, wenn Sie die Daten ändern, ohne dassquares-Array zu mutieren:
Das Ergebnis ist dasselbe, aber indem Sie nicht direkt mutieren (die zugrundeliegenden Daten ändern), erhalten Sie mehrere Vorteile.
Unveränderlichkeit macht komplexe Funktionen viel einfacher zu implementieren. Später in diesem Tutorial werden Sie eine „Zeitreise“-Funktion implementieren, mit der Sie den Spielverlauf überprüfen und zu vergangenen Zügen „zurückspringen“ können. Diese Funktionalität ist nicht auf Spiele beschränkt – die Möglichkeit, bestimmte Aktionen rückgängig zu machen und erneut auszuführen, ist eine häufige Anforderung für Apps. Durch das Vermeiden direkter Datenmutation können Sie frühere Versionen der Daten intakt halten und später wiederverwenden.
Es gibt noch einen weiteren Vorteil der Unveränderlichkeit. Standardmäßig rendern alle Kindkomponenten automatisch neu, wenn sich der State einer Elternkomponente ändert. Das schließt sogar Kindkomponenten ein, die von der Änderung nicht betroffen waren. Obwohl das Neu-Rendern für den Benutzer an sich nicht bemerkbar ist (Sie sollten nicht aktiv versuchen, es zu vermeiden!), möchten Sie möglicherweise aus Performance-Gründen das Neu-Rendern eines Teils des Baums überspringen, der eindeutig nicht davon betroffen war. Unveränderlichkeit macht es für Komponenten sehr kostengünstig, zu vergleichen, ob sich ihre Daten geändert haben oder nicht. Sie können mehr darüber erfahren, wie React entscheidet, wann eine Komponente neu gerendert werden soll, inder memo-API-Referenz.
Spielzüge abwechseln
Es ist jetzt an der Zeit, einen großen Fehler in diesem Tic-Tac-Toe-Spiel zu beheben: Die „O“s können nicht auf dem Spielfeld markiert werden.
Sie werden den ersten Zug standardmäßig auf „X“ setzen. Verfolgen wir dies, indem wir der Board-Komponente ein weiteres State-Stück hinzufügen:
Jedes Mal, wenn ein Spieler einen Zug macht, wirdxIsNext(ein Boolean) umgedreht, um zu bestimmen, welcher Spieler als Nächstes an der Reihe ist, und der Spielzustand wird gespeichert. Sie werden dieBoard-FunktionhandleClickaktualisieren, um den Wert vonxIsNextumzudrehen:
Wenn Sie nun auf verschiedene Felder klicken, wechseln sie wie vorgesehen zwischenXundO!
Aber Moment, es gibt ein Problem. Versuchen Sie, mehrmals auf dasselbe Feld zu klicken:

DasXwird durch einOüberschrieben! Obwohl dies dem Spiel eine sehr interessante Wendung geben würde, bleiben wir vorerst bei den ursprünglichen Regeln.
Wenn Sie ein Feld mit einemXoder einemOmarkieren, prüfen Sie nicht zuerst, ob das Feld bereits einenX- oderO-Wert hat. Sie können dies beheben, indem Siefrühzeitig zurückkehren. Sie prüfen, ob das Feld bereits einXoder einOenthält. Wenn das Feld bereits belegt ist, kehren Sie in derreturn-Anweisung derhandleClick-Funktion frühzeitig zurück – bevor versucht wird, den Spielzustand zu aktualisieren.
Jetzt können Sie nur nochXoderOin leere Felder setzen! So sollte Ihr Code an dieser Stelle aussehen:
Einen Gewinner ermitteln
Da die Spieler nun abwechselnd ziehen können, möchten Sie anzeigen, wann das Spiel gewonnen ist und keine weiteren Züge mehr möglich sind. Dazu fügen Sie eine Hilfsfunktion namenscalculateWinnerhinzu, die ein Array von 9 Feldern nimmt, auf einen Gewinner prüft und entsprechend'X','O'odernullzurückgibt. Machen Sie sich keine allzu großen Gedanken über die FunktioncalculateWinner; sie ist nicht spezifisch für React:
Hinweis
Es spielt keine Rolle, ob SiecalculateWinnervor oder nachBoarddefinieren. Setzen wir sie ans Ende, damit Sie nicht jedes Mal daran vorbeiscrollen müssen, wenn Sie Ihre Komponenten bearbeiten.
Sie werdencalculateWinner(squares)in derBoard-Komponente der FunktionhandleClickaufrufen, um zu prüfen, ob ein Spieler gewonnen hat. Sie können diese Prüfung gleichzeitig durchführen, wenn Sie prüfen, ob ein Benutzer auf ein Feld geklickt hat, das bereits einXoder einOenthält. Wir möchten in beiden Fällen frühzeitig zurückkehren:
Damit die Spieler wissen, wann das Spiel vorbei ist, kannst du einen Text wie „Gewinner: X“ oder „Gewinner: O“ anzeigen. Dazu fügst du einenstatus-Abschnitt zurBoard-Komponente hinzu. Der Status zeigt den Gewinner an, wenn das Spiel beendet ist, und wenn das Spiel läuft, zeigst du an, welcher Spieler als Nächster an der Reihe ist:
Herzlichen Glückwunsch! Du hast jetzt ein funktionierendes Tic-Tac-Toe-Spiel. Und du hast gerade auch die Grundlagen von React gelernt. Also bistduhier der wahre Gewinner. So sollte der Code aussehen:
Zeitreise hinzufügen
Als letzte Übung machen wir es möglich, in der Zeit zurückzugehen und vorherige Spielzüge anzuschauen.
Eine Historie der Züge speichern
Wenn du dassquares-Array mutieren würdest, wäre die Implementierung einer Zeitreise sehr schwierig.
Du hast jedochslice()verwendet, um nach jedem Zug eine neue Kopie dessquares-Arrays zu erstellen und es als unveränderlich behandelt. Dies ermöglicht es dir, jede vergangene Version dessquares-Arrays zu speichern und zwischen den bereits geschehenen Spielzügen zu navigieren.
Du wirst die vergangenensquares-Arrays in einem anderen Array namenshistoryspeichern, das du als neue Zustandsvariable speicherst. Dashistory-Array repräsentiert alle Brettzustände, vom ersten bis zum letzten Zug, und hat etwa folgende Form:
State erneut nach oben heben
Du wirst jetzt eine neue Top-Level-Komponente namensGameschreiben, um eine Liste vergangener Züge anzuzeigen. Dort wirst du denhistory-State platzieren, der die gesamte Spielhistorie enthält.
Indem du denhistory-State in dieGame-Komponente verschiebst, kannst du densquares-State aus ihrer KindkomponenteBoardentfernen. Genau wie du den State von derSquare-Komponente in dieBoard-Komponente „nach oben gehoben“ hast, wirst du ihn jetzt von derBoard-Komponente in die Top-Level-KomponenteGameheben. Dies gibt derGame-Komponente die volle Kontrolle über die Daten desBoardund ermöglicht es ihr, demBoardanzuweisen, vorherige Spielzüge aus derhistoryzu rendern.
Füge zuerst eineGame-Komponente mitexport defaulthinzu. Lasse sie dieBoard-Komponente und etwas Markup rendern:
Beachten Sie, dass Sie die Schlüsselwörterexport defaultvor der Deklarationfunction Board() {entfernen und sie vor der Deklarationfunction Game() {hinzufügen. Dies teilt Ihrer Dateiindex.jsmit, dieGame-Komponente als oberste Komponente zu verwenden, anstelle IhrerBoard-Komponente. Die zusätzlichendiv-Elemente, die von derGame-Komponente zurückgegeben werden, schaffen Platz für die Spielinformationen, die Sie später zum Brett hinzufügen werden.
Fügen Sie derGame-Komponente etwas State hinzu, um zu verfolgen, welcher Spieler als Nächstes an der Reihe ist, und den Verlauf der Züge:
Beachten Sie, dass[Array(9).fill(null)]ein Array mit einem einzelnen Element ist, das selbst ein Array aus 9null-Werten ist.
Um die Felder für den aktuellen Zug zu rendern, müssen Sie das letzte Felder-Array aus demhistoryauslesen. Sie benötigen dafür keinuseState– Sie haben bereits genügend Informationen, um es während des Renderings zu berechnen:
Erstellen Sie als Nächstes eine FunktionhandlePlayinnerhalb derGame-Komponente, die von derBoard-Komponente aufgerufen wird, um das Spiel zu aktualisieren. Übergeben SiexIsNext,currentSquaresundhandlePlayals Props an dieBoard-Komponente:
Lassen Sie uns dieBoard-Komponente vollständig durch die Props steuern, die sie erhält. Ändern Sie dieBoard-Komponente so, dass sie drei Props akzeptiert:xIsNext,squaresund eine neue FunktiononPlay, dieBoardaufrufen kann, wenn ein Spieler einen Zug macht, und dabei das aktualisierte Felder-Array übergibt. Entfernen Sie als Nächstes die ersten beiden Zeilen derBoard-Funktion, dieuseStateaufrufen:
Ersetzen Sie nun die Aufrufe vonsetSquaresundsetXIsNext in handleClickin derBoard-Komponente durch einen einzelnen Aufruf Ihrer neuen FunktiononPlay, damit dieGame-Komponente dasBoardaktualisieren kann, wenn der Benutzer auf ein Feld klickt:
DieBoard-Komponente wird vollständig durch die Props gesteuert, die ihr von derGame-Komponente übergeben werden. Sie müssen die FunktionhandlePlayin derGame-Komponente implementieren, damit das Spiel wieder funktioniert.
Was sollhandlePlaytun, wenn sie aufgerufen wird? Denken Sie daran, dass Board frühersetSquaresmit einem aktualisierten Array aufgerufen hat; jetzt übergibt es das aktualisiertesquares-Array anonPlay.
Die FunktionhandlePlaymuss den State vonGameaktualisieren, um ein erneutes Rendering auszulösen, aber Sie haben keinesetSquares-Funktion mehr, die Sie aufrufen können – Sie verwenden jetzt die State-Variablehistory, um diese Informationen zu speichern. Sie solltenhistoryaktualisieren, indem Sie das aktualisiertesquares-Array als neuen Verlaufseintrag anhängen. Sie möchten auchxIsNextumschalten, genau wie Board es früher getan hat:
Hier erstellt[...history, nextSquares]ein neues Array, das alle Elemente aushistorygefolgt vonnextSquaresenthält. (Sie können die...historySpread-Syntaxals „alle Elemente inhistory)
Wenn beispielsweisehistorygleich[[null,null,null], ["X",null,null]]undnextSquaresgleich["X",null,"O"]ist, dann wird das neue Array[...history, nextSquares] [[null,null,null], ["X",null,null], ["X",null,"O"]]sein.
An diesem Punkt haben Sie den State in dieGame-Komponente verschoben, und die Benutzeroberfläche sollte vollständig funktionieren, genau wie vor der Umstrukturierung. So sollte der Code an dieser Stelle aussehen:
Vergangene Züge anzeigen
Da Sie den Spielverlauf des Tic-Tac-Toe-Spiels aufzeichnen, können Sie dem Spieler nun eine Liste vergangener Züge anzeigen.
React-Elemente wie<button>sind reguläre JavaScript-Objekte; Sie können sie in Ihrer Anwendung herumreichen. Um mehrere Elemente in React zu rendern, können Sie ein Array von React-Elementen verwenden.
Sie haben bereits ein Array vonhistory-Zügen im State, also müssen Sie es nun in ein Array von React-Elementen umwandeln. In JavaScript können Sie, um ein Array in ein anderes zu transformieren, dieArray-Map-Methode verwenden:
Sie werdenmapverwenden, um Ihrehistoryder Züge in React-Elemente zu transformieren, die Schaltflächen auf dem Bildschirm darstellen, und eine Liste von Schaltflächen anzuzeigen, um zu vergangenen Zügen zu „springen“. Lassen Sie unsmapüber diehistoryin der Game-Komponente anwenden:
Unten können Sie sehen, wie Ihr Code aussehen sollte. Beachten Sie, dass Sie einen Fehler in der Konsole der Entwicklertools sehen sollten, der besagt:
Sie werden diesen Fehler im nächsten Abschnitt beheben.
Während du dashistory-Array in der Funktion durchläufst, die du anmapübergeben hast, durchläuft das Argumentsquaresjedes Element vonhistory, und das Argumentmovedurchläuft jeden Array-Index:0,1,2, …. (In den meisten Fällen bräuchtest du die tatsächlichen Array-Elemente, aber um eine Liste der Züge zu rendern, benötigst du nur die Indizes.)
Für jeden Zug in der Historie des Tic-Tac-Toe-Spiels erstellst du ein Listenelement<li>, das einen Button<button>enthält. Der Button hat einenonClick-Handler, der eine Funktion namensjumpToaufruft (die du noch nicht implementiert hast).
Vorerst solltest du eine Liste der Züge sehen, die im Spiel aufgetreten sind, und einen Fehler in der Konsole der Entwicklertools. Lass uns besprechen, was der "key"-Fehler bedeutet.
Auswahl eines Keys
Wenn du eine Liste renderst, speichert React einige Informationen über jedes gerenderte Listenelement. Wenn du eine Liste aktualisierst, muss React bestimmen, was sich geändert hat. Du könntest Elemente hinzugefügt, entfernt, neu angeordnet oder aktualisiert haben.
Stell dir den Übergang von
zu
Zusätzlich zu den aktualisierten Zählwerten würde ein menschlicher Leser wahrscheinlich sagen, dass du die Reihenfolge von Alexa und Ben vertauscht und Claudia zwischen Alexa und Ben eingefügt hast. React ist jedoch ein Computerprogramm und weiß nicht, was du beabsichtigt hast, daher musst du für jedes Listenelement einekey-Eigenschaft angeben, um jedes Listenelement von seinen Geschwistern zu unterscheiden. Wenn deine Daten aus einer Datenbank stammen, könnten die Datenbank-IDs von Alexa, Ben und Claudia als Keys verwendet werden.
Wenn eine Liste neu gerendert wird, nimmt React den Key jedes Listenelements und sucht in den Elementen der vorherigen Liste nach einem übereinstimmenden Key. Wenn die aktuelle Liste einen Key hat, der vorher nicht existierte, erstellt React eine Komponente. Wenn der aktuellen Liste ein Key fehlt, der in der vorherigen Liste existierte, zerstört React die vorherige Komponente. Wenn zwei Keys übereinstimmen, wird die entsprechende Komponente verschoben.
Keys teilen React die Identität jeder Komponente mit, was es React ermöglicht, den Zustand zwischen Neu-Renderings beizubehalten. Wenn sich der Key einer Komponente ändert, wird die Komponente zerstört und mit einem neuen Zustand neu erstellt.
keyist eine spezielle und reservierte Eigenschaft in React. Wenn ein Element erstellt wird, extrahiert React diekey-Eigenschaft und speichert den Key direkt auf dem zurückgegebenen Element. Auch wennkeyso aussieht, als würde es als Props übergeben, verwendet Reactkeyautomatisch, um zu entscheiden, welche Komponenten aktualisiert werden sollen. Es gibt keine Möglichkeit für eine Komponente, nachzufragen, welchenkeyihr übergeordnetes Element angegeben hat.
Es wird dringend empfohlen, dass du immer dann geeignete Keys zuweist, wenn du dynamische Listen erstellst.Wenn du keinen passenden Key hast, solltest du in Betracht ziehen, deine Daten umzustrukturieren, damit du einen hast.
Wenn kein Key angegeben ist, wird React einen Fehler melden und standardmäßig den Array-Index als Key verwenden. Die Verwendung des Array-Index als Key ist problematisch, wenn du versuchst, die Elemente einer Liste neu anzuordnen oder Listenelemente einzufügen/zu entfernen. Explizites Übergeben vonkey={i}unterdrückt den Fehler, hat aber die gleichen Probleme wie Array-Indizes und wird in den meisten Fällen nicht empfohlen.
Keys müssen nicht global eindeutig sein; sie müssen nur zwischen Komponenten und ihren Geschwistern eindeutig sein.
Implementierung von Zeitreisen
In der Historie des Tic-Tac-Toe-Spiels hat jeder vergangene Zug eine eindeutige ID, die ihm zugeordnet ist: Es ist die fortlaufende Nummer des Zuges. Züge werden niemals neu angeordnet, gelöscht oder in der Mitte eingefügt, daher ist es sicher, den Zugindex als Key zu verwenden.
In derGame-Funktion kannst du den Key als<li key={move}>hinzufügen, und wenn du das gerenderte Spiel neu lädst, sollte der "key"-Fehler von React verschwinden:
Bevor SiejumpToimplementieren können, muss dieGame-Komponente verfolgen, welchen Schritt der Benutzer gerade betrachtet. Definieren Sie dazu eine neue Zustandsvariable namenscurrentMove, die standardmäßig auf0gesetzt ist:
Aktualisieren Sie als Nächstes die FunktionjumpToinnerhalb vonGame, um diesencurrentMovezu aktualisieren. Sie setzen auchxIsNextauftrue, wenn die Zahl, auf die SiecurrentMoveändern, gerade ist.
Sie werden nun zwei Änderungen an derGame-FunktionhandlePlayvornehmen, die aufgerufen wird, wenn Sie auf ein Feld klicken.
- Wenn Sie „in der Zeit zurückgehen“ und von diesem Punkt aus einen neuen Zug machen, möchten Sie nur den Verlauf bis zu diesem Punkt behalten. Statt
nextSquaresnach allen Elementen (...Spread-Syntax) inhistoryhinzuzufügen, fügen Sie es nach allen Elementen inhistory.slice(0, currentMove + 1)hinzu, sodass Sie nur diesen Teil des alten Verlaufs behalten. - Jedes Mal, wenn ein Zug gemacht wird, müssen Sie
currentMoveaktualisieren, damit es auf den neuesten Verlaufseintrag zeigt.
Schließlich werden Sie dieGame-Komponente so ändern, dass sie den aktuell ausgewählten Zug rendert, anstatt immer den letzten Zug:
Wenn Sie auf einen beliebigen Schritt in der Spielhistorie klicken, sollte das Tic-Tac-Toe-Brett sofort aktualisiert werden, um den Zustand des Bretts nach diesem Schritt anzuzeigen.
Abschließende Bereinigung
Wenn Sie den Code sehr genau betrachten, stellen Sie vielleicht fest, dassxIsNext === trueist, wenncurrentMovegerade ist, undxIsNext === false, wenncurrentMoveungerade ist. Mit anderen Worten: Wenn Sie den Wert voncurrentMovekennen, können Sie immer herausfinden, wasxIsNextsein sollte.
Es gibt keinen Grund, beide im State zu speichern. Versuchen Sie grundsätzlich, redundanten State zu vermeiden. Eine Vereinfachung dessen, was Sie im State speichern, reduziert Fehler und macht Ihren Code leichter verständlich. Ändern SieGameso, dass esxIsNextnicht als separate State-Variable speichert, sondern stattdessen basierend auf demcurrentMoveberechnet:
Sie benötigen diexIsNext-State-Deklaration oder die Aufrufe vonsetXIsNextnicht mehr. Jetzt besteht keine Möglichkeit mehr, dassxIsNextnicht mehr mitcurrentMovesynchron ist, selbst wenn Sie beim Programmieren der Komponenten einen Fehler machen.
Abschluss
Herzlichen Glückwunsch! Sie haben ein Tic-Tac-Toe-Spiel erstellt, das:
- Sie Tic-Tac-Toe spielen lässt,
- anzeigt, wenn ein Spieler das Spiel gewonnen hat,
- den Spielverlauf während des Spiels speichert,
- es Spielern ermöglicht, den Spielverlauf einzusehen und frühere Versionen des Spielbretts zu sehen.
Gute Arbeit! Wir hoffen, Sie haben jetzt das Gefühl, ein solides Verständnis dafür zu haben, wie React funktioniert.
Sehen Sie sich hier das Endergebnis an:
Wenn Sie zusätzliche Zeit haben oder Ihre neuen React-Fähigkeiten üben möchten, finden Sie hier einige Ideen für Verbesserungen, die Sie am Tic-Tac-Toe-Spiel vornehmen könnten, aufgelistet in der Reihenfolge zunehmender Schwierigkeit:
- Zeigen Sie für den aktuellen Zug nur „Sie sind bei Zug #…“ anstelle eines Buttons an.
- Schreiben Sie die
Board-Komponente um, um zwei Schleifen zu verwenden, um die Quadrate zu erstellen, anstatt sie fest zu kodieren. - Fügen Sie einen Umschaltbutton hinzu, der es Ihnen ermöglicht, die Züge in aufsteigender oder absteigender Reihenfolge zu sortieren.
- Wenn jemand gewinnt, heben Sie die drei Quadrate hervor, die den Sieg verursacht haben (und wenn niemand gewinnt, zeigen Sie eine Meldung über ein Unentschieden an).
- Zeigen Sie die Position für jeden Zug im Format (Zeile, Spalte) in der Zugverlaufsliste an.
Während dieses Tutorials haben Sie React-Konzepte wie Elemente, Komponenten, Props und State kennengelernt. Nachdem Sie gesehen haben, wie diese Konzepte beim Erstellen eines Spiels funktionieren, werfen Sie einen Blick aufDenken in React, um zu sehen, wie dieselben React-Konzepte beim Erstellen einer Benutzeroberfläche einer App funktionieren.
