Tutoriel : Tic-Tac-Toe
Vous allez construire un petit jeu de tic-tac-toe pendant ce tutoriel. Ce tutoriel ne suppose aucune connaissance préalable de React. Les techniques que vous apprendrez dans ce tutoriel sont fondamentales pour construire n'importe quelle application React, et en comprendre pleinement le contenu vous donnera une compréhension approfondie de React.
Note
Ce tutoriel est conçu pour les personnes qui préfèrentapprendre en faisantet qui veulent rapidement essayer de créer quelque chose de concret. Si vous préférez apprendre chaque concept étape par étape, commencez parDécrire l'interface utilisateur.
Le tutoriel est divisé en plusieurs sections :
- Configuration pour le tutorielvous donneraun point de départpour suivre le tutoriel.
- Vue d'ensemblevous enseignerales fondamentauxde React : les composants, les props et l'état.
- Compléter le jeuvous enseignerales techniques les plus courantesdu développement React.
- Ajouter le voyage dans le tempsvous donneraun aperçu plus approfondides forces uniques de React.
Que construisez-vous ?
Dans ce tutoriel, vous allez construire un jeu de tic-tac-toe interactif avec React.
Vous pouvez voir à quoi cela ressemblera une fois terminé ici :
Si le code ne vous semble pas encore clair, ou si vous n'êtes pas familier avec la syntaxe du code, ne vous inquiétez pas ! L'objectif de ce tutoriel est de vous aider à comprendre React et sa syntaxe.
Nous vous recommandons de consulter le jeu de tic-tac-toe ci-dessus avant de poursuivre le tutoriel. L'une des fonctionnalités que vous remarquerez est qu'il y a une liste numérotée à droite du plateau de jeu. Cette liste vous donne un historique de tous les coups qui se sont produits dans le jeu, et elle est mise à jour au fur et à mesure que le jeu progresse.
Une fois que vous avez exploré le jeu de tic-tac-toe terminé, continuez à faire défiler. Vous commencerez avec un modèle plus simple dans ce tutoriel. Notre prochaine étape est de vous configurer pour que vous puissiez commencer à construire le jeu.
Configuration pour le tutoriel
Dans l'éditeur de code en direct ci-dessous, cliquez surForkdans le coin supérieur droit pour ouvrir l'éditeur dans un nouvel onglet en utilisant le site CodeSandbox. CodeSandbox vous permet d'écrire du code dans votre navigateur et de prévisualiser comment vos utilisateurs verront l'application que vous avez créée. Le nouvel onglet devrait afficher un carré vide et le code de départ pour ce tutoriel.
Note
Vous pouvez également suivre ce tutoriel en utilisant votre environnement de développement local. Pour ce faire, vous devez :
- InstallerNode.js
- Dans l'onglet CodeSandbox que vous avez ouvert précédemment, appuyez sur le bouton en haut à gauche pour ouvrir le menu, puis choisissezTélécharger le bac à sabledans ce menu pour télécharger une archive des fichiers localement
- Dézippez l'archive, puis ouvrez un terminal et
cddans le répertoire que vous avez dézippé - Installez les dépendances avec
npm install - Exécutez
npm startpour démarrer un serveur local et suivez les instructions pour voir le code s'exécuter dans un navigateur
Si vous êtes bloqué, ne laissez pas cela vous arrêter ! Suivez plutôt en ligne et réessayez une configuration locale plus tard.
Vue d'ensemble
Maintenant que vous êtes configuré, faisons un tour d'horizon de React !
Inspection du code de départ
Dans CodeSandbox, vous verrez trois sections principales :

- La sectionFichiersavec une liste de fichiers comme
App.js,index.js,styles.cssdans le dossiersrcet un dossier appelépublic - L'éditeur de codeoù vous verrez le code source de votre fichier sélectionné
- La sectionnavigateuroù vous verrez comment le code que vous avez écrit sera affiché
Le fichierApp.jsdevrait être sélectionné dans la sectionFichiers. Le contenu de ce fichier dans l'éditeur de codedevrait être :
La sectionnavigateurdevrait afficher un carré avec un X à l'intérieur, comme ceci :

Maintenant, jetons un coup d'œil aux fichiers du code de démarrage.
App.js
Le code dansApp.jscrée uncomposant. Dans React, un composant est un morceau de code réutilisable qui représente une partie d'une interface utilisateur. Les composants sont utilisés pour afficher, gérer et mettre à jour les éléments de l'interface utilisateur dans votre application. Examinons le composant ligne par ligne pour voir ce qui se passe :
La première ligne définit une fonction appeléeSquare. Le mot-clé JavaScriptexportrend cette fonction accessible en dehors de ce fichier. Le mot-clédefaultindique aux autres fichiers utilisant votre code qu'il s'agit de la fonction principale de votre fichier.
La deuxième ligne retourne un bouton. Le mot-clé JavaScriptreturnsignifie que ce qui suit est retourné comme valeur à l'appelant de la fonction.<button>est unélément JSX. Un élément JSX est une combinaison de code JavaScript et de balises HTML qui décrit ce que vous souhaitez afficher.className="square"est une propriété de bouton oupropqui indique au CSS comment styliser le bouton.Xest le texte affiché à l'intérieur du bouton et</button>ferme l'élément JSX pour indiquer que tout contenu suivant ne doit pas être placé à l'intérieur du bouton.
styles.css
Cliquez sur le fichier intituléstyles.cssdans la sectionFichiersde CodeSandbox. Ce fichier définit les styles de votre application React. Les deux premierssélecteurs CSS(*etbody) définissent le style de grandes parties de votre application, tandis que le sélecteur.squaredéfinit le style de tout composant dont la propriétéclassNameest définie sursquare. Dans votre code, cela correspondrait au bouton de votre composant Square dans le fichierApp.js.
index.js
Cliquez sur le fichier intituléindex.jsdans la sectionFichiersde CodeSandbox. Vous n'éditerez pas ce fichier pendant le tutoriel, mais il fait le lien entre le composant que vous avez créé dans le fichierApp.jset le navigateur web.
Les lignes 1 à 5 rassemblent toutes les pièces nécessaires :
- React
- La bibliothèque de React pour communiquer avec les navigateurs web (React DOM)
- les styles de vos composants
- le composant que vous avez créé dans
App.js.
Le reste du fichier assemble toutes les pièces et injecte le produit final dansindex.htmldu dossierpublic.
Construction du plateau
Revenons àApp.js. C’est là que vous passerez le reste du tutoriel.
Actuellement, le plateau n’est qu’une seule case, mais vous en avez besoin de neuf ! Si vous essayez simplement de copier-coller votre case pour en faire deux comme ceci :
Vous obtiendrez cette erreur :
Console
/src/App.js : Les éléments JSX adjacents doivent être enveloppés dans une balise englobante. Vouliez-vous un fragment JSX<>...</>?
Les composants React doivent retourner un seul élément JSX et non plusieurs éléments JSX adjacents comme deux boutons. Pour corriger cela, vous pouvez utiliser desfragments(<>et</>) pour envelopper plusieurs éléments JSX adjacents comme ceci :
Maintenant, vous devriez voir :

Super ! Maintenant, il vous suffit de copier-coller quelques fois pour ajouter neuf cases et…

Oh non ! Les cases sont toutes sur une seule ligne, pas dans une grille comme vous en avez besoin pour notre plateau. Pour corriger cela, vous devrez regrouper vos cases en lignes avec desdivet ajouter quelques classes CSS. Tant que vous y êtes, vous donnerez un numéro à chaque case pour vous assurer de savoir où chaque case est affichée.
Dans le fichierApp.js, mettez à jour le composantSquarepour qu’il ressemble à ceci :
Le CSS défini dansstyles.cssstylise les divs ayant laclassName board-row. Maintenant que vous avez regroupé vos composants en lignes avec lesdivstylisés, vous avez votre plateau de morpion :

Mais vous avez maintenant un problème. Votre composant nomméSquaren’est plus vraiment une case. Corrigeons cela en changeant le nom enBoard:
À ce stade, votre code devrait ressembler à quelque chose comme ceci :
Remarque
Psssst… C’est beaucoup à taper ! C’est normal de copier-coller du code depuis cette page. Cependant, si vous êtes prêt pour un petit défi, nous vous recommandons de ne copier que le code que vous avez tapé manuellement au moins une fois vous-même.
Passage de données via les props
Ensuite, vous voudrez changer la valeur d’une case de vide à « X » lorsque l’utilisateur clique sur la case. Avec la façon dont vous avez construit le plateau jusqu’à présent, vous devriez copier-coller le code qui met à jour la case neuf fois (une fois pour chaque case que vous avez) ! Au lieu de copier-coller, l’architecture des composants React vous permet de créer un composant réutilisable pour éviter un code désordonné et dupliqué.
Premièrement, vous allez copier la ligne définissant votre première case (<button className="square">1</button>) de votre composantBoarddans un nouveau composantSquare :
Ensuite, vous allez mettre à jour le composant Board pour qu'il rende ce composantSquareen utilisant la syntaxe JSX :
Notez que contrairement auxdivdu navigateur, vos propres composantsBoardetSquaredoivent commencer par une majuscule.
Jetons un coup d'œil :

Oh non ! Vous avez perdu les cases numérotées que vous aviez auparavant. Maintenant, chaque case affiche « 1 ». Pour corriger cela, vous allez utiliser lespropspour transmettre la valeur que chaque case doit avoir du composant parent (Board) à son enfant (Square).
Mettez à jour le composantSquarepour qu'il lise la propvalueque vous allez passer depuis le composantBoard:
function Square({ value })indique que le composant Square peut recevoir une prop appeléevalue.
Maintenant, vous voulez afficher cettevalueau lieu de1à l'intérieur de chaque case. Essayez de le faire comme ceci :
Oups, ce n'est pas ce que vous vouliez :

Vous vouliez afficher la variable JavaScript appeléevaluedepuis votre composant, et non le mot « value ». Pour « sortir vers JavaScript » depuis JSX, vous avez besoin d'accolades. Ajoutez des accolades autour devaluedans le JSX comme ceci :
Pour l'instant, vous devriez voir un plateau vide :

C'est parce que le composantBoardn'a pas encore transmis la propvalueà chaque composantSquarequ'il rend. Pour corriger cela, vous allez ajouter la propvalueà chaque composantSquarerendu par le composantBoard :
Maintenant, vous devriez voir à nouveau une grille de nombres :

Votre code mis à jour devrait ressembler à ceci :
Créer un composant interactif
Remplissons le composantSquareavec unXlorsque vous cliquez dessus. Déclarez une fonction appeléehandleClickà l'intérieur du composantSquare. Ensuite, ajoutezonClickaux props de l'élément JSX button retourné par le composantSquare:
Si vous cliquez sur une case maintenant, vous devriez voir un message"clicked!"dans l'ongletConsoleen bas de la sectionNavigateurdans CodeSandbox. Cliquer plusieurs fois sur la même case affichera à nouveau"clicked!". Des messages de console répétés avec le même texte ne créent pas de nouvelles lignes dans la console. À la place, vous verrez un compteur incrémenté à côté de votre premier message"clicked!".
Remarque
Si vous suivez ce tutoriel en utilisant votre environnement de développement local, vous devez ouvrir la Console de votre navigateur. Par exemple, si vous utilisez le navigateur Chrome, vous pouvez afficher la Console avec le raccourci clavierMaj + Ctrl + J(sous Windows/Linux) ouOption + ⌘ + J(sous macOS).
L'étape suivante consiste à faire en sorte que le composant Square « se souvienne » qu'il a été cliqué et qu'il affiche une marque « X ». Pour « se souvenir » des choses, les composants utilisent l'état.
React fournit une fonction spéciale appeléeuseStateque vous pouvez appeler depuis votre composant pour lui permettre de « se souvenir » des choses. Stockons la valeur actuelle duSquaredans l'état, et modifions-la lorsque leSquareest cliqué.
ImportezuseStateen haut du fichier. Supprimez la propvaluedu composantSquare. À la place, ajoutez une nouvelle ligne au début du composantSquarequi appelleuseState. Faites en sorte qu'elle retourne une variable d'état appeléevalue:
valuestocke la valeur etsetValueest une fonction qui peut être utilisée pour modifier la valeur. Lenullpassé àuseStateest utilisé comme valeur initiale de cette variable d'état, doncvaluecommence ici par être égal ànull.
Puisque le composantSquaren'accepte plus de props, vous allez supprimer la propvaluedes neuf composants Square créés par le composant Board :
Maintenant, vous allez modifier le composantSquarepour qu'il affiche un « X » lorsqu'il est cliqué. Remplacez le gestionnaire d'événementconsole.log("clicked!"); par setValue('X');. Votre composantSquareressemble maintenant à ceci :
En appelant cette fonctionsetdepuis un gestionnaireonClick, vous indiquez à React de re-rendre ceSquarechaque fois que son<button>est cliqué. Après la mise à jour, lavalueduSquaresera'X', donc vous verrez le « X » sur le plateau de jeu. Cliquez sur n'importe quelle case, et « X » devrait apparaître :

Chaque Square a son propre état : lavaluestockée dans chaque Square est complètement indépendante des autres. Lorsque vous appelez une fonctionsetdans un composant, React met automatiquement à jour les composants enfants à l'intérieur également.
Après avoir effectué les modifications ci-dessus, votre code ressemblera à ceci :
React Developer Tools
React DevTools vous permet de vérifier les props et l'état de vos composants React. Vous pouvez trouver l'onglet React DevTools en bas de la sectionnavigateurdans CodeSandbox :

Pour inspecter un composant particulier à l'écran, utilisez le bouton en haut à gauche de React DevTools :

Compléter le jeu
À ce stade, vous avez tous les éléments de base pour votre jeu de morpion. Pour avoir un jeu complet, vous devez maintenant alterner la pose des « X » et des « O » sur le plateau, et vous avez besoin d'un moyen de déterminer un gagnant.
Remonter l'état
Actuellement, chaque composantSquaremaintient une partie de l'état du jeu. Pour vérifier s'il y a un gagnant dans un jeu de morpion, le composantBoardaurait besoin de connaître d'une manière ou d'une autre l'état de chacun des 9 composantsSquare.
Comment aborderiez-vous cela ? Au début, vous pourriez penser que le composantBoarddoit « demander » à chaque composantSquareson étatBoard plutôt que dans chaque composant Square. Le composantBoardpeut indiquer à chaque composantSquarequoi afficher en passant une prop, comme vous l'avez fait lorsque vous avez passé un nombre à chaque Square.
Pour collecter des données de plusieurs enfants, ou pour que deux composants enfants communiquent entre eux, déclarez l'état partagé dans leur composant parent. Le composant parent peut ensuite transmettre cet état aux enfants via des props. Cela maintient les composants enfants synchronisés entre eux et avec leur parent.
Remonter l'état dans un composant parent est courant lors de la refactorisation de composants React.
Profitons de cette occasion pour l'essayer. Modifiez le composantBoardpour qu'il déclare une variable d'état nomméesquaresqui est par défaut un tableau de 9 nulls correspondant aux 9 cases :
Array(9).fill(null)crée un tableau de neuf éléments et définit chacun d'eux ànull. L'appeluseState()autour de cela déclare une variable d'étatsquaresqui est initialement définie sur ce tableau. Chaque entrée du tableau correspond à la valeur d'une case. Lorsque vous remplirez le plateau plus tard, le tableausquaresressemblera à ceci :
Maintenant, votre composantBoarddoit passer la propvalueà chaque composantSquarequ'il rend :
Ensuite, vous allez modifier le composantSquarepour qu'il reçoive la propvaluedu composant Board. Cela nécessitera de supprimer le suivi avec état de la valeurvaluepar le composant Square et la proponClickdu bouton :
À ce stade, vous devriez voir un plateau de morpion vide :

Et votre code devrait ressembler à ceci :
Chaque case recevra maintenant une propvaluequi sera soit'X','O', soitnullpour les cases vides.
Ensuite, vous devez modifier ce qui se passe lorsqu’uneSquareest cliquée. Le composantBoardmaintient maintenant l’état des cases remplies. Vous devez créer un moyen pour que laSquaremette à jour l’état duBoard. Comme l’état est privé au composant qui le définit, vous ne pouvez pas mettre à jour l’état duBoarddirectement depuisSquare.
Au lieu de cela, vous allez passer une fonction du composantBoardau composantSquare, et vous ferez en sorte queSquareappelle cette fonction lorsqu’une case est cliquée. Vous commencerez par la fonction que le composantSquareappellera lorsqu’il est cliqué. Vous appellerez cette fonctiononSquareClick:
Ensuite, vous ajouterez la fonctiononSquareClickaux props du composantSquare :
Maintenant, vous allez connecter la proponSquareClickà une fonction dans le composantBoardque vous nommerezhandleClick. Pour connecteronSquareClick à handleClick, vous passerez une fonction à la proponSquareClickde la première caseSquare :
Enfin, vous définirez la fonctionhandleClickà l’intérieur du composant Board pour mettre à jour le tableausquaresqui contient l’état de votre plateau :
La fonctionhandleClickcrée une copie du tableausquares (nextSquares) avec la méthode JavaScriptslice()des tableaux. Ensuite,handleClickmet à jour le tableaunextSquarespour ajouter unXà la première case (index[0]).
L’appel de la fonctionsetSquaresindique à React que l’état du composant a changé. Cela déclenchera un nouveau rendu des composants qui utilisent l’étatsquares (Board) ainsi que de ses composants enfants (les composantsSquarequi constituent le plateau).
Note
JavaScript prend en charge lesfermetures, ce qui signifie qu’une fonction interne (par exemplehandleClick) a accès aux variables et fonctions définies dans une fonction externe (par exempleBoard). La fonctionhandleClickpeut lire l’étatsquareset appeler la méthodesetSquarescar elles sont toutes deux définies à l’intérieur de la fonctionBoard.
Maintenant, vous pouvez ajouter des X au plateau… mais uniquement dans la case supérieure gauche. Votre fonctionhandleClickest codée en dur pour mettre à jour l’index de la case supérieure gauche (0). Mettons à jourhandleClickpour qu’elle puisse mettre à jour n’importe quelle case. Ajoutez un argumentià la fonctionhandleClickqui prend l’index de la case à mettre à jour :
Ensuite, vous devez passer ceti à handleClick. Vous pourriez essayer de définir la proponSquareClickdu carré comme étanthandleClick(0)directement dans le JSX comme ceci, mais cela ne fonctionnera pas :
Voici pourquoi cela ne fonctionne pas. L’appelhandleClick(0)fera partie du rendu du composant plateau. Parce quehandleClick(0)modifie l’état du composant plateau en appelantsetSquares, votre composant plateau entier sera rendu à nouveau. Mais cela exécute à nouveauhandleClick(0), ce qui conduit à une boucle infinie :
Console
Trop de rendus. React limite le nombre de rendus pour éviter une boucle infinie.
Pourquoi ce problème ne s’est-il pas produit plus tôt ?
Lorsque vous passiezonSquareClick={handleClick}, vous passiez la fonctionhandleClicken tant que prop. Vous ne l’appeliez pas ! Mais maintenant, vousappelezcette fonction immédiatement—remarquez les parenthèses danshandleClick(0)—et c’est pourquoi elle s’exécute trop tôt. Vous nevoulezpas appelerhandleClickavant que l’utilisateur ne clique !
Vous pourriez résoudre ce problème en créant une fonction commehandleFirstSquareClickqui appellehandleClick(0), une fonction commehandleSecondSquareClickqui appellehandleClick(1), et ainsi de suite. Vous passeriez (plutôt que d’appeler) ces fonctions en tant que props commeonSquareClick={handleFirstSquareClick}. Cela résoudrait la boucle infinie.
Cependant, définir neuf fonctions différentes et donner un nom à chacune d’elles est trop verbeux. À la place, faisons ceci :
Remarquez la nouvelle syntaxe() =>. Ici,() => handleClick(0)est unefonction fléchée,qui est une manière plus courte de définir des fonctions. Lorsque le carré est cliqué, le code après la=>« flèche » s’exécutera, appelanthandleClick(0).
Maintenant, vous devez mettre à jour les huit autres carrés pour qu’ils appellenthandleClickdepuis les fonctions fléchées que vous passez. Assurez-vous que l’argument pour chaque appel dehandleClickcorrespond à l’index du carré correct :
Maintenant, vous pouvez à nouveau ajouter des X à n’importe quel carré du plateau en cliquant dessus :

Mais cette fois, toute la gestion de l’état est prise en charge par le composantBoard !
Voici à quoi votre code devrait ressembler :
Maintenant que votre gestion de l’état se trouve dans le composantBoard, le composant parentBoardpasse des props aux composants enfantsSquareafin qu’ils puissent être affichés correctement. Lorsqu’on clique sur unSquare, le composant enfantSquaredemande maintenant au composant parentBoardde mettre à jour l’état du plateau. Lorsque l’état duBoardchange, le composantBoardet chaque enfantSquaresont automatiquement rendus à nouveau. Garder l’état de tous les carrés dans le composantBoardpermettra de déterminer le gagnant à l’avenir.
Récapitulons ce qui se passe lorsqu’un utilisateur clique sur le carré en haut à gauche de votre plateau pour y ajouter unX :
- Cliquer sur le carré en haut à gauche exécute la fonction que le
buttona reçue comme proponClickdepuis le composantSquare. Le composantSquarea reçu cette fonction comme proponSquareClickdepuis le composantBoard. Le composantBoarda défini cette fonction directement dans le JSX. Il appellehandleClickavec l'argument0. handleClickutilise l'argument (0) pour mettre à jour le premier élément du tableausquaresdenullàX.- L'état
squaresdu composantBoardayant été mis à jour, le composantBoardet tous ses enfants sont re-rendus. Cela entraîne le changement de la propvaluedu composantSquareavec l'index0denullàX.
Au final, l'utilisateur voit que le carré en haut à gauche est passé de vide à contenir unXaprès avoir cliqué dessus.
Note
L'attribut<button> onClickde l'élément DOM a une signification particulière pour React car il s'agit d'un composant intégré. Pour les composants personnalisés comme Square, le nommage vous appartient. Vous pouvez donner n'importe quel nom à la proponSquareClickdu composantSquareou à la fonctionhandleClickdu composantBoard, et le code fonctionnerait de la même manière. Dans React, il est conventionnel d'utiliser des nomsonSomethingpour les props qui représentent des événements ethandleSomethingpour les définitions de fonctions qui gèrent ces événements.
Pourquoi l'immuabilité est importante
Remarquez comment danshandleClick, vous appelez.slice()pour créer une copie du tableausquaresau lieu de modifier le tableau existant. Pour expliquer pourquoi, nous devons discuter de l'immuabilité et de son importance à apprendre.
Il existe généralement deux approches pour modifier des données. La première approche consiste àmuterles données en changeant directement leurs valeurs. La seconde approche consiste à remplacer les données par une nouvelle copie qui contient les modifications souhaitées. Voici à quoi cela ressemblerait si vous mutiez le tableausquares :
Et voici à quoi cela ressemblerait si vous changiez les données sans muter le tableausquares :
Le résultat est le même, mais en évitant de muter (changer les données sous-jacentes) directement, vous obtenez plusieurs avantages.
L'immuabilité rend les fonctionnalités complexes beaucoup plus faciles à implémenter. Plus loin dans ce tutoriel, vous implémenterez une fonctionnalité de « voyage dans le temps » qui vous permet de revoir l'historique du jeu et de « revenir en arrière » sur les coups passés. Cette fonctionnalité n'est pas spécifique aux jeux — la capacité d'annuler et de refaire certaines actions est une exigence courante pour les applications. Éviter la mutation directe des données vous permet de conserver intactes les versions précédentes des données et de les réutiliser plus tard.
L'immuabilité présente également un autre avantage. Par défaut, tous les composants enfants sont re-rendus automatiquement lorsque l'état d'un composant parent change. Cela inclut même les composants enfants qui n'ont pas été affectés par le changement. Bien que le re-rendu ne soit pas en soi perceptible par l'utilisateur (vous ne devriez pas chercher activement à l'éviter !), vous pourriez vouloir sauter le re-rendu d'une partie de l'arbre qui n'a clairement pas été affectée pour des raisons de performance. L'immuabilité rend très peu coûteux pour les composants de comparer si leurs données ont changé ou non. Vous pouvez en apprendre davantage sur la façon dont React choisit quand re-rendre un composant dansla référence de l'API memo.
Tour à tour
Il est maintenant temps de corriger un défaut majeur dans ce jeu de morpion : les « O » ne peuvent pas être marqués sur le plateau.
Vous définirez le premier coup comme étant « X » par défaut. Gardons une trace de cela en ajoutant un autre élément d'état au composant Board :
Chaque fois qu'un joueur joue,xIsNext(un booléen) sera inversé pour déterminer quel joueur joue ensuite et l'état du jeu sera sauvegardé. Vous allez mettre à jour la fonctionhandleClickduBoardpour inverser la valeur dexIsNext:
Maintenant, lorsque vous cliquez sur différentes cases, elles alterneront entreXetO, comme il se doit !
Mais attendez, il y a un problème. Essayez de cliquer plusieurs fois sur la même case :

LeXest écrasé par unO! Bien que cela ajouterait une tournure très intéressante au jeu, nous allons nous en tenir aux règles originales pour l'instant.
Lorsque vous marquez une case avec unXou unO, vous ne vérifiez pas d'abord si la case a déjà une valeurXouO. Vous pouvez corriger cela enretournant prématurément. Vous allez vérifier si la case a déjà unXou unO. Si la case est déjà remplie, vous allez faire unreturndans la fonctionhandleClickprématurément — avant qu'elle n'essaie de mettre à jour l'état du plateau.
Maintenant, vous ne pouvez ajouter desXou desOque dans les cases vides ! Voici à quoi votre code devrait ressembler à ce stade :
Déclarer un gagnant
Maintenant que les joueurs peuvent jouer à tour de rôle, vous voudrez afficher quand le jeu est gagné et qu'il n'y a plus de tours à jouer. Pour ce faire, vous allez ajouter une fonction d'aide appeléecalculateWinnerqui prend un tableau de 9 cases, vérifie s'il y a un gagnant et retourne'X','O', ounullselon le cas. Ne vous inquiétez pas trop de la fonctioncalculateWinner; elle n'est pas spécifique à React :
Note
Peu importe que vous définissiezcalculateWinneravant ou après leBoard. Mettons-la à la fin pour que vous n'ayez pas à la défiler à chaque fois que vous modifiez vos composants.
Vous allez appelercalculateWinner(squares)dans la fonctionhandleClickdu composantBoardpour vérifier si un joueur a gagné. Vous pouvez effectuer cette vérification en même temps que vous vérifiez si un utilisateur a cliqué sur une case qui a déjà unXou unO. Nous aimerions retourner prématurément dans les deux cas :
Pour informer les joueurs de la fin de la partie, vous pouvez afficher un texte tel que « Gagnant : X » ou « Gagnant : O ». Pour cela, vous allez ajouter une sectionstatusau composantBoard. Le statut affichera le gagnant si la partie est terminée, et si elle est en cours, il indiquera quel joueur doit jouer ensuite :
Félicitations ! Vous avez maintenant un jeu de morpion fonctionnel. Et vous venez également d'apprendre les bases de React. Doncvousêtes le vrai gagnant ici. Voici à quoi devrait ressembler le code :
Ajouter un voyage dans le temps
Pour terminer, rendons possible de « remonter dans le temps » pour revenir aux coups précédents de la partie.
Stocker un historique des coups
Si vous aviez muté le tableausquares, la mise en œuvre du voyage dans le temps serait très difficile.
Cependant, vous avez utiliséslice()pour créer une nouvelle copie du tableausquaresaprès chaque coup, et l'avez traité comme immuable. Cela vous permettra de stocker chaque version passée du tableausquareset de naviguer entre les tours déjà joués.
Vous allez stocker les tableauxsquarespassés dans un autre tableau appeléhistory, que vous stockerez comme une nouvelle variable d'état. Le tableauhistoryreprésente tous les états du plateau, du premier au dernier coup, et a une forme comme celle-ci :
Remonter l'état, encore une fois
Vous allez maintenant écrire un nouveau composant de niveau supérieur appeléGamepour afficher une liste des coups passés. C'est là que vous placerez l'étathistoryqui contient l'historique complet de la partie.
Placer l'étathistorydans le composantGamevous permettra de supprimer l'étatsquaresde son composant enfantBoard. Tout comme vous avez « remonté l'état » du composantSquarevers le composantBoard, vous allez maintenant le remonter du composantBoardvers le composant de niveau supérieurGame. Cela donne au composantGameun contrôle total sur les données duBoardet lui permet d'ordonner auBoardde rendre les tours précédents à partir de l'history.
Tout d'abord, ajoutez un composantGame avec export default. Faites-le rendre le composantBoardet un peu de balisage :
Notez que vous supprimez les mots-clésexport defaultavant la déclarationfunction Board() {et les ajoutez avant la déclarationfunction Game() {. Cela indique à votre fichierindex.jsd'utiliser le composantGamecomme composant de niveau supérieur au lieu de votre composantBoard. Les divsupplémentaires renvoyés par le composantGamepréparent l'espace pour les informations du jeu que vous ajouterez au plateau plus tard.
Ajoutez un état au composantGamepour suivre le prochain joueur et l'historique des coups :
Notez que[Array(9).fill(null)]est un tableau avec un seul élément, qui est lui-même un tableau de 9null.
Pour afficher les cases du coup actuel, vous devez lire le dernier tableau de cases depuis l'history. Vous n'avez pas besoin deuseStatepour cela—vous avez déjà suffisamment d'informations pour le calculer lors du rendu :
Ensuite, créez une fonctionhandlePlayà l'intérieur du composantGamequi sera appelée par le composantBoardpour mettre à jour le jeu. PassezxIsNext,currentSquaresethandlePlaycomme props au composantBoard :
Rendons le composantBoardentièrement contrôlé par les props qu'il reçoit. Modifiez le composantBoardpour qu'il prenne trois props :xIsNext,squares, et une nouvelle fonctiononPlay que Boardpeut appeler avec le tableau de cases mis à jour lorsqu'un joueur effectue un coup. Ensuite, supprimez les deux premières lignes de la fonctionBoardqui appellentuseState:
Maintenant, remplacez les appels àsetSquaresetsetXIsNextdanshandleClickdu composantBoardpar un seul appel à votre nouvelle fonctiononPlayafin que le composantGamepuisse mettre à jour leBoardlorsque l'utilisateur clique sur une case :
Le composantBoardest entièrement contrôlé par les props qui lui sont passées par le composantGame. Vous devez implémenter la fonctionhandlePlaydans le composantGamepour que le jeu fonctionne à nouveau.
Que doit fairehandlePlaylorsqu'elle est appelée ? Rappelez-vous que Board appelait auparavantsetSquaresavec un tableau mis à jour ; maintenant, il passe le tableausquaresmis à jour àonPlay.
La fonctionhandlePlaydoit mettre à jour l'état deGamepour déclencher un nouveau rendu, mais vous n'avez plus de fonctionsetSquaresà appeler—vous utilisez maintenant la variable d'étathistorypour stocker ces informations. Vous devez mettre à jourhistoryen ajoutant le tableausquaresmis à jour comme une nouvelle entrée d'historique. Vous devez aussi basculerxIsNext, comme le faisait auparavant Board :
Ici,[...history, nextSquares]crée un nouveau tableau qui contient tous les éléments dehistory, suivi denextSquares. (Vous pouvez lire la...historysyntaxe de décompositioncomme « énumérer tous les éléments dehistory».)
Par exemple, sihistoryest[[null,null,null], ["X",null,null]]et quenextSquaresest["X",null,"O"], alors le nouveau tableau[...history, nextSquares] sera [[null,null,null], ["X",null,null], ["X",null,"O"]].
À ce stade, vous avez déplacé l'état pour qu'il réside dans le composantGame, et l'interface utilisateur devrait fonctionner pleinement, comme avant la refactorisation. Voici à quoi devrait ressembler le code à ce stade :
Afficher les coups passés
Puisque vous enregistrez l'historique du jeu de morpion, vous pouvez maintenant afficher une liste des coups passés au joueur.
Les éléments React comme<button>sont des objets JavaScript ordinaires ; vous pouvez les faire circuler dans votre application. Pour afficher plusieurs éléments dans React, vous pouvez utiliser un tableau d'éléments React.
Vous avez déjà un tableau de coupshistorydans l'état, donc maintenant vous devez le transformer en un tableau d'éléments React. En JavaScript, pour transformer un tableau en un autre, vous pouvez utiliser laméthode map des tableaux :
Vous allez utilisermappour transformer votrehistoryde coups en éléments React représentant des boutons à l'écran, et afficher une liste de boutons pour « sauter » vers des coups passés. Appliquonsmapsur l'historydans le composant Game :
Vous pouvez voir à quoi votre code devrait ressembler ci-dessous. Notez que vous devriez voir une erreur dans la console des outils de développement qui dit :
Vous corrigerez cette erreur dans la section suivante.
Lorsque vous parcourez le tableauhistoryà l'intérieur de la fonction que vous avez passée àmap, l'argumentsquarespasse par chaque élément dehistory, et l'argumentmovepasse par chaque index du tableau :0,1,2, …. (Dans la plupart des cas, vous auriez besoin des éléments réels du tableau, mais pour afficher une liste des coups, vous n'aurez besoin que des index.)
Pour chaque coup dans l'historique du jeu de morpion, vous créez un élément de liste<li>qui contient un bouton<button>. Le bouton a un gestionnaireonClickqui appelle une fonction nomméejumpTo(que vous n'avez pas encore implémentée).
Pour l'instant, vous devriez voir une liste des coups survenus dans le jeu et une erreur dans la console des outils de développement. Discutons de ce que signifie l'erreur « key ».
Choisir une clé
Lorsque vous affichez une liste, React stocke des informations sur chaque élément de liste rendu. Lorsque vous mettez à jour une liste, React doit déterminer ce qui a changé. Vous pourriez avoir ajouté, supprimé, réorganisé ou mis à jour les éléments de la liste.
Imaginez la transition de
à
En plus des comptes mis à jour, un humain lisant cela dirait probablement que vous avez inversé l'ordre d'Alexa et Ben et inséré Claudia entre Alexa et Ben. Cependant, React est un programme informatique et ne sait pas ce que vous avez prévu, donc vous devez spécifier une propriétékeypour chaque élément de liste afin de différencier chaque élément de liste de ses frères et sœurs. Si vos données provenaient d'une base de données, les ID de base de données d'Alexa, Ben et Claudia pourraient être utilisés comme clés.
Lorsqu'une liste est re-rendue, React prend la clé de chaque élément de liste et recherche dans les éléments de la liste précédente une clé correspondante. Si la liste actuelle a une clé qui n'existait pas auparavant, React crée un composant. Si la liste actuelle manque une clé qui existait dans la liste précédente, React détruit le composant précédent. Si deux clés correspondent, le composant correspondant est déplacé.
Les clés indiquent à React l'identité de chaque composant, ce qui permet à React de maintenir l'état entre les re-rendus. Si la clé d'un composant change, le composant sera détruit et recréé avec un nouvel état.
keyest une propriété spéciale et réservée dans React. Lorsqu'un élément est créé, React extrait la propriétékeyet stocke la clé directement sur l'élément retourné. Même sikeypeut sembler être passé comme props, React utilise automatiquementkeypour décider quels composants mettre à jour. Il n'y a aucun moyen pour un composant de demander quellekeyson parent a spécifiée.
Il est fortement recommandé d'attribuer des clés appropriées chaque fois que vous construisez des listes dynamiques.Si vous n'avez pas de clé appropriée, vous devriez envisager de restructurer vos données pour en avoir une.
Si aucune clé n'est spécifiée, React signalera une erreur et utilisera l'index du tableau comme clé par défaut. Utiliser l'index du tableau comme clé est problématique lorsque vous essayez de réorganiser les éléments d'une liste ou d'insérer/supprimer des éléments de liste. Passer explicitementkey={i}fait taire l'erreur mais présente les mêmes problèmes que les indices de tableau et n'est pas recommandé dans la plupart des cas.
Les clés n'ont pas besoin d'être globalement uniques ; elles doivent uniquement être uniques entre les composants et leurs frères et sœurs.
Implémenter le voyage dans le temps
Dans l'historique du jeu de morpion, chaque coup passé a un ID unique associé : c'est le numéro séquentiel du coup. Les coups ne seront jamais réordonnés, supprimés ou insérés au milieu, il est donc sûr d'utiliser l'index du coup comme clé.
Dans la fonctionGame, vous pouvez ajouter la clé sous la forme<li key={move}>, et si vous rechargez le jeu rendu, l'erreur « key » de React devrait disparaître :
Avant de pouvoir implémenterjumpTo, le composantGamedoit garder trace de l'étape que l'utilisateur est en train de visualiser. Pour cela, définissez une nouvelle variable d'état appeléecurrentMove, par défaut à0:
Ensuite, mettez à jour la fonctionjumpToà l'intérieur deGamepour modifier cecurrentMove. Vous définirez égalementxIsNext à truesi le nombre vers lequel vous changezcurrentMoveest pair.
Vous allez maintenant apporter deux modifications à la fonctionhandlePlaydu composantGame, qui est appelée lorsque vous cliquez sur une case.
- Si vous « remontez dans le temps » et que vous effectuez ensuite un nouveau coup à partir de ce point, vous ne souhaitez conserver que l'historique jusqu'à ce point. Au lieu d'ajouter
nextSquaresaprès tous les éléments (syntaxe de décomposition...) dehistory, vous l'ajouterez après tous les éléments dehistory.slice(0, currentMove + 1)afin de ne conserver que cette portion de l'ancien historique. - Chaque fois qu'un coup est joué, vous devez mettre à jour
currentMovepour qu'il pointe vers la dernière entrée de l'historique.
Enfin, vous allez modifier le composantGamepour qu'il affiche le coup actuellement sélectionné, au lieu de toujours afficher le dernier coup :
Si vous cliquez sur n'importe quelle étape de l'historique du jeu, le plateau de tic-tac-toe doit immédiatement se mettre à jour pour afficher l'état du plateau après cette étape.
Nettoyage final
Si vous examinez le code de très près, vous remarquerez peut-être quexIsNext === truelorsquecurrentMoveest pair etxIsNext === falselorsquecurrentMoveest impair. En d'autres termes, si vous connaissez la valeur decurrentMove, vous pouvez toujours déterminer ce quexIsNextdevrait être.
Il n'y a aucune raison de stocker ces deux valeurs dans l'état. En fait, essayez toujours d'éviter les états redondants. Simplifier ce que vous stockez dans l'état réduit les bugs et rend votre code plus facile à comprendre. ModifiezGamepour qu'il ne stocke pasxIsNextcomme une variable d'état séparée et qu'il le détermine plutôt en fonction ducurrentMove:
Vous n'avez plus besoin de la déclaration d'étatxIsNextni des appels àsetXIsNext. Maintenant, il n'y a aucune chance quexIsNextse désynchronise aveccurrentMove, même si vous faites une erreur en codant les composants.
Récapitulatif
Félicitations ! Vous avez créé un jeu de morpion qui :
- Vous permet de jouer au morpion,
- Indique quand un joueur a gagné la partie,
- Stocke l'historique d'une partie au fur et à mesure de son déroulement,
- Permet aux joueurs de revoir l'historique d'une partie et de voir les versions précédentes du plateau de jeu.
Beau travail ! Nous espérons que vous avez maintenant l'impression de bien comprendre le fonctionnement de React.
Consultez le résultat final ici :
Si vous avez du temps supplémentaire ou souhaitez pratiquer vos nouvelles compétences en React, voici quelques idées d'améliorations que vous pourriez apporter au jeu du morpion, listées par ordre de difficulté croissante :
- Pour le coup actuel uniquement, affichez « Vous êtes au coup #… » au lieu d'un bouton.
- Réécrivez
Boardpour utiliser deux boucles afin de créer les cases au lieu de les coder en dur. - Ajoutez un bouton de bascule qui permet de trier les coups par ordre croissant ou décroissant.
- Lorsque quelqu'un gagne, mettez en surbrillance les trois cases qui ont causé la victoire (et lorsque personne ne gagne, affichez un message indiquant que le résultat est une égalité).
- Affichez l'emplacement de chaque coup au format (ligne, colonne) dans la liste de l'historique des coups.
Tout au long de ce tutoriel, vous avez abordé des concepts de React, notamment les éléments, les composants, les props et l'état. Maintenant que vous avez vu comment ces concepts fonctionnent lors de la création d'un jeu, consultezPenser en Reactpour voir comment les mêmes concepts React fonctionnent lors de la création de l'interface utilisateur d'une application.
