v19.2Latest

Tres en raya

Construirás un pequeño juego de tres en raya durante este tutorial. Este tutorial no asume ningún conocimiento previo de React. Las técnicas que aprenderás en el tutorial son fundamentales para construir cualquier aplicación React, y comprenderlo completamente te dará un entendimiento profundo de React.

Nota

Este tutorial está diseñado para personas que prefierenaprender haciendoy quieren probar rápidamente a crear algo tangible. Si prefieres aprender cada concepto paso a paso, comienza conDescribir la interfaz de usuario.

El tutorial está dividido en varias secciones:

¿Qué vas a construir?

En este tutorial, construirás un juego interactivo de tres en raya con React.

Puedes ver cómo se verá cuando termines aquí:

Si el código aún no tiene sentido para ti, o si no estás familiarizado con la sintaxis del código, ¡no te preocupes! El objetivo de este tutorial es ayudarte a entender React y su sintaxis.

Te recomendamos que revises el juego de tres en raya de arriba antes de continuar con el tutorial. Una de las características que notarás es que hay una lista numerada a la derecha del tablero del juego. Esta lista te da un historial de todos los movimientos que han ocurrido en el juego, y se actualiza a medida que el juego progresa.

Una vez que hayas jugado con el juego de tres en raya terminado, sigue desplazándote. Comenzarás con una plantilla más simple en este tutorial. Nuestro siguiente paso es prepararte para que puedas comenzar a construir el juego.

Configuración para el tutorial

En el editor de código en vivo a continuación, haz clic enForken la esquina superior derecha para abrir el editor en una nueva pestaña usando el sitio web CodeSandbox. CodeSandbox te permite escribir código en tu navegador y previsualizar cómo tus usuarios verán la aplicación que has creado. La nueva pestaña debería mostrar un cuadrado vacío y el código inicial para este tutorial.

Nota

También puedes seguir este tutorial usando tu entorno de desarrollo local. Para hacer esto, necesitas:

  1. InstalarNode.js
  2. En la pestaña de CodeSandbox que abriste antes, presiona el botón de la esquina superior izquierda para abrir el menú, y luego eligeDescargar Sandboxen ese menú para descargar un archivo de los archivos localmente
  3. Descomprime el archivo, luego abre una terminal ycdal directorio que descomprimiste
  4. Instala las dependencias connpm install
  5. Ejecutanpm startpara iniciar un servidor local y sigue las indicaciones para ver el código ejecutándose en un navegador

Si te quedas atascado, ¡no dejes que esto te detenga! Sigue en línea y prueba la configuración local nuevamente más tarde.

Descripción general

Ahora que estás configurado, ¡obtengamos una descripción general de React!

Inspeccionando el código inicial

En CodeSandbox verás tres secciones principales:

CodeSandbox con código inicial
  1. La secciónArchivoscon una lista de archivos comoApp.js,index.js,styles.cssen la carpetasrcy una carpeta llamadapublic
  2. Eleditor de códigodonde verás el código fuente del archivo seleccionado
  3. La sección delnavegadordonde verás cómo se mostrará el código que has escrito

El archivoApp.jsdebería estar seleccionado en la secciónArchivos. El contenido de ese archivo en eleditor de códigodebería ser:

La sección delnavegadordebería mostrar un cuadrado con una X dentro, así:

cuadrado con una X

Ahora echemos un vistazo a los archivos del código inicial.

App.js

El código enApp.jscrea uncomponente. En React, un componente es una pieza de código reutilizable que representa una parte de una interfaz de usuario. Los componentes se utilizan para renderizar, gestionar y actualizar los elementos de la UI en tu aplicación. Veamos el componente línea por línea para entender qué está pasando:

La primera línea define una función llamadaSquare. La palabra clave de JavaScriptexporthace que esta función sea accesible fuera de este archivo. La palabra clavedefaultle indica a otros archivos que usen tu código que es la función principal en tu archivo.

La segunda línea devuelve un botón. La palabra clave de JavaScriptreturnsignifica que lo que viene después se devuelve como un valor a quien llama a la función.<button>es unelemento JSX. Un elemento JSX es una combinación de código JavaScript y etiquetas HTML que describe lo que te gustaría mostrar.className="square"es una propiedad del botón opropque le dice a CSS cómo estilizar el botón.Xes el texto que se muestra dentro del botón y</button>cierra el elemento JSX para indicar que cualquier contenido posterior no debe colocarse dentro del botón.

styles.css

Haz clic en el archivo etiquetadostyles.cssen la secciónArchivosde CodeSandbox. Este archivo define los estilos para tu aplicación React. Los dos primerosselectores CSS(* y body) definen el estilo de partes grandes de tu aplicación, mientras que el selector.squaredefine el estilo de cualquier componente donde la propiedadclassNameesté configurada comosquare. En tu código, eso coincidiría con el botón de tu componente Square en el archivoApp.js.

index.js

Haz clic en el archivo etiquetadoindex.jsen la secciónArchivosde CodeSandbox. No editarás este archivo durante el tutorial, pero es el puente entre el componente que creaste en el archivoApp.jsy el navegador web.

Las líneas 1-5 unen todas las piezas necesarias:

  • React
  • La biblioteca de React para comunicarse con navegadores web (React DOM)
  • los estilos para tus componentes
  • el componente que creaste enApp.js.

El resto del archivo une todas las piezas e inyecta el producto final enindex.htmldentro de la carpetapublic.

Construyendo el tablero

Volvamos aApp.js. Aquí es donde pasarás el resto del tutorial.

Actualmente el tablero es solo un cuadrado, ¡pero necesitas nueve! Si solo intentas copiar y pegar tu cuadrado para hacer dos cuadrados así:

Obtendrás este error:

Consola

/src/App.js: Los elementos JSX adyacentes deben estar envueltos en una etiqueta envolvente. ¿Querías un Fragmento JSX<>...</>?

Los componentes de React deben devolver un solo elemento JSX y no múltiples elementos JSX adyacentes como dos botones. Para solucionarlo puedes usarFragmentos(<> y </>) para envolver múltiples elementos JSX adyacentes así:

Ahora deberías ver:

dos cuadrados rellenos con X

¡Genial! Ahora solo necesitas copiar-pegar unas cuantas veces para agregar nueve cuadrados y…

nueve cuadrados rellenos con X en una línea

¡Oh no! Los cuadrados están todos en una sola línea, no en una cuadrícula como necesitas para nuestro tablero. Para solucionarlo necesitarás agrupar tus cuadrados en filas condivs y agregar algunas clases CSS. Mientras lo haces, le darás un número a cada cuadrado para asegurarte de saber dónde se muestra cada uno.

En el archivoApp.js, actualiza el componenteSquarepara que se vea así:

El CSS definido enstyles.cssestiliza los divs con laclassNamedeboard-row. Ahora que has agrupado tus componentes en filas con losdivs estilizados, tienes tu tablero de tres en raya:

tablero de tres en raya lleno con números del 1 al 9

Pero ahora tienes un problema. Tu componente llamadoSquare, realmente ya no es un cuadrado. Arreglemos eso cambiando el nombre aBoard:

En este punto tu código debería verse algo así:

Nota

Psssst… ¡Eso es mucho para escribir! Está bien copiar y pegar código de esta página. Sin embargo, si estás listo para un pequeño desafío, recomendamos solo copiar código que hayas escrito manualmente al menos una vez tú mismo.

Pasando datos a través de props

A continuación, querrás cambiar el valor de un cuadrado de vacío a “X” cuando el usuario haga clic en él. Con la forma en que has construido el tablero hasta ahora, ¡necesitarías copiar-pegar el código que actualiza el cuadrado nueve veces (una por cada cuadrado que tienes)! En lugar de copiar-pegar, la arquitectura de componentes de React te permite crear un componente reutilizable para evitar código duplicado y desordenado.

Primero, vas a copiar la línea que define tu primer cuadrado (<button className="square">1</button>) de tu componenteBoarda un nuevo componenteSquare:

Luego actualizarás el componente Board para renderizar ese componenteSquareusando la sintaxis JSX:

Observa cómo, a diferencia de losdivdel navegador, tus propios componentesBoard y Squaredeben comenzar con una letra mayúscula.

Echemos un vistazo:

tablero lleno de unos

¡Oh, no! Perdiste los cuadrados numerados que tenías antes. Ahora cada cuadrado dice "1". Para solucionarlo, usaráspropspara pasar el valor que cada cuadrado debe tener desde el componente padre (Board) a su hijo (Square).

Actualiza el componenteSquarepara leer la propvalueque pasarás desde elBoard:

function Square({ value })indica que al componente Square se le puede pasar una prop llamadavalue.

Ahora quieres mostrar esevalueen lugar de1dentro de cada cuadrado. Intenta hacerlo así:

Ups, esto no es lo que querías:

tablero lleno de la palabra value

Querías renderizar la variable JavaScript llamadavaluedesde tu componente, no la palabra "value". Para "escapar a JavaScript" desde JSX, necesitas llaves. Agrega llaves alrededor devalueen JSX así:

Por ahora, deberías ver un tablero vacío:

tablero vacío

Esto se debe a que el componenteBoardaún no ha pasado la propvaluea cada componenteSquareque renderiza. Para solucionarlo, agregarás la propvaluea cada componenteSquarerenderizado por el componenteBoard:

Ahora deberías ver una cuadrícula de números nuevamente:

tablero de tres en raya lleno con números del 1 al 9

Tu código actualizado debería verse así:

Crear un componente interactivo

Llenemos el componenteSquarecon unaXcuando hagas clic en él. Declara una función llamadahandleClickdentro delSquare. Luego, agregaonClicka las props del elemento JSX del botón devuelto por elSquare:

Si haces clic en un cuadrado ahora, deberías ver un registro que dice"clicked!"en la pestañaConsolaen la parte inferior de la secciónNavegadoren CodeSandbox. Hacer clic en el cuadrado más de una vez registrará"clicked!"de nuevo. Los registros repetidos de la consola con el mismo mensaje no crearán más líneas en la consola. En su lugar, verás un contador incremental junto a tu primer registro"clicked!".

Nota

Si estás siguiendo este tutorial usando tu entorno de desarrollo local, necesitas abrir la Consola de tu navegador. Por ejemplo, si usas el navegador Chrome, puedes ver la Consola con el atajo de tecladoShift + Ctrl + J(en Windows/Linux) oOpción + ⌘ + J(en macOS).

Como siguiente paso, quieres que el componente Square "recuerde" que se hizo clic en él y lo llene con una marca "X". Para "recordar" cosas, los componentes usanestado.

React proporciona una función especial llamadauseStateque puedes llamar desde tu componente para que "recuerde" cosas. Almacenemos el valor actual delSquareen el estado, y cambiémoslo cuando se haga clic en elSquare.

ImportauseStateen la parte superior del archivo. Elimina la propvaluedel componenteSquare. En su lugar, agrega una nueva línea al inicio delSquareque llama auseState. Haz que devuelva una variable de estado llamadavalue:

valuealmacena el valor ysetValuees una función que se puede usar para cambiar el valor. Elnullpasado auseStatese usa como el valor inicial para esta variable de estado, por lo quevalueaquí comienza siendo igual anull.

Dado que el componenteSquareya no acepta props, eliminarás la propvaluede los nueve componentes Square creados por el componente Board:

Ahora cambiarásSquarepara que muestre una "X" cuando se haga clic. Reemplaza el controlador de eventosconsole.log("clicked!"); con setValue('X');. Ahora tu componenteSquarese ve así:

Al llamar a esta funciónsetdesde un controladoronClick, le estás diciendo a React que vuelva a renderizar eseSquarecada vez que se haga clic en su<button>. Después de la actualización, elvaluedelSquareserá'X', por lo que verás la "X" en el tablero de juego. Haz clic en cualquier Square, y debería aparecer una "X":

agregando equis al tablero

Cada Square tiene su propio estado: elvaluealmacenado en cada Square es completamente independiente de los demás. Cuando llamas a una funciónseten un componente, React actualiza automáticamente los componentes hijos dentro también.

Después de realizar los cambios anteriores, tu código se verá así:

React Developer Tools

React DevTools te permite verificar las props y el estado de tus componentes React. Puedes encontrar la pestaña de React DevTools en la parte inferior de la secciónbrowseren CodeSandbox:

React DevTools en CodeSandbox

Para inspeccionar un componente particular en la pantalla, usa el botón en la esquina superior izquierda de React DevTools:

Seleccionando componentes en la página con React DevTools
Nota

Para desarrollo local, React DevTools está disponible como una extensión de navegador paraChrome,Firefox y Edge. Instálala, y la pestañaComponentsaparecerá en las Herramientas de Desarrollo de tu navegador para sitios que usan React.

Completando el juego

En este punto, tienes todos los bloques básicos para tu juego de tres en raya. Para tener un juego completo, ahora necesitas alternar la colocación de "X" y "O" en el tablero, y necesitas una forma de determinar un ganador.

Elevar el estado

Actualmente, cada componenteSquaremantiene una parte del estado del juego. Para verificar si hay un ganador en un juego de tres en raya, el componenteBoardnecesitaría saber de alguna manera el estado de cada uno de los 9 componentesSquare.

¿Cómo abordarías eso? Al principio, podrías adivinar que el componenteBoardnecesita "preguntarle" a cada componenteSquarepor su estadoBoard en lugar de en cada componente Square. El componenteBoardpuede decirle a cada componenteSquarequé mostrar pasando una prop, como hiciste cuando pasaste un número a cada Square.

Para recopilar datos de múltiples hijos, o para que dos componentes hijos se comuniquen entre sí, declara el estado compartido en su componente padre. El componente padre puede pasar ese estado a los hijos a través de props. Esto mantiene a los componentes hijos sincronizados entre sí y con su padre.

Elevar el estado a un componente padre es común cuando se refactorizan componentes React.

Aprovechemos esta oportunidad para probarlo. Edita el componenteBoardpara que declare una variable de estado llamadasquaresque por defecto sea un array de 9 nulls correspondientes a los 9 cuadrados:

Array(9).fill(null)crea un array con nueve elementos y establece cada uno de ellos ennull. La llamadauseState()alrededor de él declara una variable de estadosquaresque inicialmente se establece en ese array. Cada entrada en el array corresponde al valor de un cuadrado. Cuando llenes el tablero más adelante, el arraysquaresse verá así:

Ahora tu componenteBoardnecesita pasar la propvaluea cada componenteSquareque renderiza:

A continuación, editarás el componenteSquarepara recibir la propvaluedel componente Board. Esto requerirá eliminar el seguimiento con estado propio del componente Square devaluey la proponClickdel botón:

En este punto deberías ver un tablero de tres en raya vacío:

tablero vacío

Y tu código debería verse así:

Cada Square ahora recibirá una propvalueque será'X','O', onullpara los cuadrados vacíos.

A continuación, necesitas cambiar lo que sucede cuando se hace clic en unSquare. El componenteBoardahora mantiene qué cuadrados están llenos. Necesitarás crear una forma para que elSquareactualice el estado delBoard. Dado que el estado es privado para el componente que lo define, no puedes actualizar el estado delBoarddirectamente desdeSquare.

En su lugar, pasarás una función desde el componenteBoardal componenteSquare, y harás queSquarellame a esa función cuando se haga clic en un cuadrado. Comenzarás con la función que el componenteSquarellamará cuando se haga clic en él. Llamarás a esa funciónonSquareClick:

A continuación, agregarás la funciónonSquareClicka las props del componenteSquare:

Ahora conectarás la proponSquareClicka una función en el componenteBoardque llamaráshandleClick. Para conectaronSquareClick a handleClickpasarás una función a la proponSquareClickdel primer componenteSquare:

Por último, definirás la funciónhandleClickdentro del componente Board para actualizar el arraysquaresque contiene el estado de tu tablero:

La funciónhandleClickcrea una copia del arraysquares (nextSquares) con el método de array de JavaScriptslice(). Luego,handleClickactualiza el arraynextSquarespara agregarXal primer cuadrado (índice[0]).

Llamar a la funciónsetSquaresle permite a React saber que el estado del componente ha cambiado. Esto desencadenará una nueva renderización de los componentes que usan el estadosquares (Board) así como de sus componentes hijos (los componentesSquareque forman el tablero).

Nota

JavaScript admitecierres, lo que significa que una función interna (por ejemplo,handleClick) tiene acceso a variables y funciones definidas en una función externa (por ejemplo,Board). La funciónhandleClickpuede leer el estadosquaresy llamar al métodosetSquaresporque ambos están definidos dentro de la funciónBoard.

Ahora puedes agregar X al tablero… pero solo al cuadrado superior izquierdo. Tu funciónhandleClickestá codificada para actualizar el índice del cuadrado superior izquierdo (0). ActualicemoshandleClickpara que pueda actualizar cualquier cuadrado. Agrega un argumentoia la funciónhandleClickque tome el índice del cuadrado a actualizar:

A continuación, necesitas pasar esei a handleClick. Podrías intentar establecer la proponSquareClickdel cuadrado comohandleClick(0)directamente en el JSX así, pero no funcionará:

Esta es la razón por la que esto no funciona. La llamadahandleClick(0)será parte del renderizado del componente del tablero. Debido a quehandleClick(0)altera el estado del componente del tablero al llamar asetSquares, todo tu componente del tablero se volverá a renderizar. Pero esto ejecutahandleClick(0)nuevamente, lo que lleva a un bucle infinito:

Consola

Demasiados re-renderizados. React limita el número de renderizados para evitar un bucle infinito.

¿Por qué no ocurrió este problema antes?

Cuando estabas pasandoonSquareClick={handleClick}, estabas pasando la funciónhandleClickcomo una prop. ¡No la estabas llamando! Pero ahora estásllamandoesa función de inmediato—fíjate en los paréntesis enhandleClick(0)—y por eso se ejecuta demasiado pronto. Noquieresllamar ahandleClickhasta que el usuario haga clic.

Podrías solucionar esto creando una función comohandleFirstSquareClickque llame ahandleClick(0), una función comohandleSecondSquareClickque llame ahandleClick(1), y así sucesivamente. Pasarías (en lugar de llamar) estas funciones como props, comoonSquareClick={handleFirstSquareClick}. Esto resolvería el bucle infinito.

Sin embargo, definir nueve funciones diferentes y darle un nombre a cada una es demasiado verboso. En su lugar, hagamos esto:

Observa la nueva sintaxis() =>. Aquí,() => handleClick(0)es unafunción flecha,que es una forma más corta de definir funciones. Cuando se hace clic en el cuadrado, se ejecutará el código después de la=>“flecha”, llamando ahandleClick(0).

Ahora necesitas actualizar los otros ocho cuadrados para llamar ahandleClickdesde las funciones flecha que pasas. Asegúrate de que el argumento para cada llamada dehandleClickcorresponda al índice del cuadrado correcto:

Ahora puedes volver a agregar X a cualquier cuadrado del tablero haciendo clic en ellos:

llenando el tablero con X

Pero esta vez toda la gestión del estado es manejada por el componenteBoard.

Así debería verse tu código:

Ahora que tu gestión del estado está en el componenteBoard, el componente padreBoardpasa props a los componentes hijosSquarepara que puedan mostrarse correctamente. Al hacer clic en unSquare, el componente hijoSquareahora le pide al componente padreBoardque actualice el estado del tablero. Cuando cambia el estado delBoard, tanto el componenteBoardcomo cada componente hijoSquarese vuelven a renderizar automáticamente. Mantener el estado de todos los cuadrados en el componenteBoardpermitirá determinar al ganador en el futuro.

Recapitulemos lo que sucede cuando un usuario hace clic en el cuadrado superior izquierdo de tu tablero para agregar unaX:

  1. Al hacer clic en el cuadrado superior izquierdo se ejecuta la función que elbuttonrecibió como su proponClickdesde el componenteSquare. El componenteSquarerecibió esa función como su proponSquareClickdesde elBoard. El componenteBoarddefinió esa función directamente en el JSX. Llama ahandleClickcon un argumento de0.
  2. handleClickusa el argumento (0) para actualizar el primer elemento del arraysquares de null a X.
  3. El estadosquaresdel componenteBoardse actualizó, por lo que elBoardy todos sus hijos se vuelven a renderizar. Esto hace que la propvaluedel componenteSquarecon índice0cambie denull a X.

Al final, el usuario ve que el cuadrado superior izquierdo ha cambiado de estar vacío a tener unaXdespués de hacer clic en él.

Nota

El atributo<button>del elemento DOMonClicktiene un significado especial para React porque es un componente incorporado. Para componentes personalizados como Square, la nomenclatura depende de ti. Puedes dar cualquier nombre a la proponSquareClickdeSquareo a la funciónhandleClickdeBoard, y el código funcionaría igual. En React, es convencional usar nombresonSomethingpara props que representan eventos yhandleSomethingpara las definiciones de funciones que manejan esos eventos.

Por qué es importante la inmutabilidad

Observa cómo enhandleClick, llamas a.slice()para crear una copia del arraysquaresen lugar de modificar el array existente. Para explicar por qué, necesitamos discutir la inmutabilidad y por qué es importante aprenderla.

Generalmente hay dos enfoques para cambiar datos. El primer enfoque esmutarlos datos cambiando directamente sus valores. El segundo enfoque es reemplazar los datos con una nueva copia que tenga los cambios deseados. Así es como se vería si mutaras el arraysquares:

Y así es como se vería si cambiaras los datos sin mutar el arraysquares:

El resultado es el mismo, pero al no mutar (cambiar los datos subyacentes) directamente, obtienes varios beneficios.

La inmutabilidad hace que las funciones complejas sean mucho más fáciles de implementar. Más adelante en este tutorial, implementarás una función de "viaje en el tiempo" que te permite revisar el historial del juego y "retroceder" a movimientos pasados. Esta funcionalidad no es específica de los juegos: la capacidad de deshacer y rehacer ciertas acciones es un requisito común para las aplicaciones. Evitar la mutación directa de datos te permite mantener intactas las versiones anteriores de los datos y reutilizarlas más tarde.

También hay otro beneficio de la inmutabilidad. Por defecto, todos los componentes hijos se vuelven a renderizar automáticamente cuando cambia el estado de un componente padre. Esto incluye incluso los componentes hijos que no se vieron afectados por el cambio. Aunque el re-renderizado en sí no es perceptible para el usuario (¡no deberías intentar activamente evitarlo!), es posible que quieras omitir el re-renderizado de una parte del árbol que claramente no se vio afectada por razones de rendimiento. La inmutabilidad hace que sea muy económico para los componentes comparar si sus datos han cambiado o no. Puedes aprender más sobre cómo React elige cuándo volver a renderizar un componente enla referencia de la API memo.

Turnos

Ahora es el momento de corregir un defecto importante en este juego de tres en raya: las "O" no se pueden marcar en el tablero.

Establecerás que el primer movimiento sea "X" por defecto. Llevemos un registro de esto añadiendo otra pieza de estado al componente Board:

Cada vez que un jugador se mueve,xIsNext(un booleano) se invertirá para determinar qué jugador va a continuación y se guardará el estado del juego. Actualizarás la funciónBoard’shandleClickpara invertir el valor dexIsNext:

Ahora, al hacer clic en diferentes cuadrados, alternarán entreX y O, ¡como deberían!

Pero espera, hay un problema. Intenta hacer clic en el mismo cuadrado varias veces:

O sobrescribiendo una X

¡LaXes sobrescrita por unaO! Si bien esto añadiría un giro muy interesante al juego, por ahora nos ceñiremos a las reglas originales.

Cuando marcas un cuadrado con unaXo unaO, no estás verificando primero si el cuadrado ya tiene un valorX o O. Puedes solucionar estodevolviendo temprano. Verificarás si el cuadrado ya tiene unaXo unaO. Si el cuadrado ya está lleno, harás unreturnen la funciónhandleClicktemprano, antes de que intente actualizar el estado del tablero.

¡Ahora solo puedes añadirX’s oO’s a cuadrados vacíos! Así es como debería verse tu código en este punto:

Declarar un ganador

Ahora que los jugadores pueden turnarse, querrás mostrar cuándo el juego está ganado y no hay más turnos que hacer. Para ello, añadirás una función auxiliar llamadacalculateWinnerque toma un array de 9 cuadrados, verifica si hay un ganador y devuelve'X','O', onullsegún corresponda. No te preocupes demasiado por la funcióncalculateWinner; no es específica de React:

Nota

No importa si definescalculateWinnerantes o después delBoard. Pongámosla al final para que no tengas que desplazarte más allá cada vez que edites tus componentes.

Llamarás acalculateWinner(squares)en la funciónBoarddel componentehandleClickpara verificar si un jugador ha ganado. Puedes realizar esta verificación al mismo tiempo que verificas si un usuario ha hecho clic en un cuadrado que ya tiene unaXo unaO. Nos gustaría devolver temprano en ambos casos:

Para que los jugadores sepan cuándo termina el juego, puedes mostrar texto como "Ganador: X" o "Ganador: O". Para hacerlo, agregarás una secciónstatusal componenteBoard. El estado mostrará al ganador si el juego terminó, y si el juego está en curso, mostrarás de qué jugador es el siguiente turno:

¡Felicidades! Ahora tienes un juego de tres en raya funcional. Y también acabas de aprender los conceptos básicos de React. Así queeres el verdadero ganador aquí. Así es como debería verse el código:

Agregar viaje en el tiempo

Como ejercicio final, hagamos posible "retroceder en el tiempo" a los movimientos anteriores del juego.

Almacenar un historial de movimientos

Si mutaras el arraysquares, implementar el viaje en el tiempo sería muy difícil.

Sin embargo, usasteslice()para crear una nueva copia del arraysquaresdespués de cada movimiento, y lo trataste como inmutable. Esto te permitirá almacenar cada versión pasada del arraysquaresy navegar entre los turnos que ya han ocurrido.

Almacenarás los arrays pasados desquaresen otro array llamadohistory, que almacenarás como una nueva variable de estado. El arrayhistoryrepresenta todos los estados del tablero, desde el primer movimiento hasta el último, y tiene una forma como esta:

Elevar el estado nuevamente

Ahora escribirás un nuevo componente de nivel superior llamadoGamepara mostrar una lista de movimientos pasados. Allí es donde colocarás el estadohistoryque contiene todo el historial del juego.

Colocar el estadohistoryen el componenteGamete permitirá eliminar el estadosquaresde su componente hijoBoard. Al igual que "elevaste el estado" del componenteSquareal componenteBoard, ahora lo elevarás desde elBoardal componente de nivel superiorGame. Esto le da al componenteGamecontrol total sobre los datos delBoardy le permite instruir alBoardpara que renderice turnos anteriores desde elhistory.

Primero, agrega un componenteGame con export default. Haz que renderice el componenteBoardy algo de marcado:

Observa que estás eliminando las palabras claveexport defaultantes de la declaraciónfunction Board() {y añadiéndolas antes de la declaraciónfunction Game() {. Esto le indica a tu archivoindex.jsque use el componenteGamecomo el componente de nivel superior en lugar de tu componenteBoard. Los divadicionales devueltos por el componenteGameestán dejando espacio para la información del juego que añadirás al tablero más adelante.

Añade algo de estado al componenteGamepara rastrear qué jugador es el siguiente y el historial de movimientos:

Observa cómo[Array(9).fill(null)]es un array con un solo elemento, que a su vez es un array de 9nulls.

Para renderizar los cuadrados del movimiento actual, querrás leer el último array de cuadrados delhistory. No necesitasuseStatepara esto—ya tienes suficiente información para calcularlo durante el renderizado:

A continuación, crea una funciónhandlePlaydentro del componenteGameque será llamada por el componenteBoardpara actualizar el juego. PasaxIsNext,currentSquares y handlePlaycomo props al componenteBoard:

Hagamos que el componenteBoardesté completamente controlado por las props que recibe. Cambia el componenteBoardpara que tome tres props:xIsNext,squares, y una nueva funciónonPlay que Boardpuede llamar con el array de cuadrados actualizado cuando un jugador hace un movimiento. Luego, elimina las dos primeras líneas de la funciónBoardque llaman auseState:

Ahora reemplaza las llamadas asetSquares y setXIsNext en handleClicken el componenteBoardcon una sola llamada a tu nueva funciónonPlaypara que el componenteGamepueda actualizar elBoardcuando el usuario haga clic en un cuadrado:

El componenteBoardestá completamente controlado por las props que le pasa el componenteGame. Necesitas implementar la funciónhandlePlayen el componenteGamepara que el juego vuelva a funcionar.

¿Qué debería hacerhandlePlaycuando se llama? Recuerda que Board solía llamar asetSquarescon un array actualizado; ahora pasa el arraysquaresactualizado aonPlay.

La funciónhandlePlaynecesita actualizar el estado deGamepara desencadenar un nuevo renderizado, pero ya no tienes una funciónsetSquaresque puedas llamar—ahora estás usando la variable de estadohistorypara almacenar esta información. Querrás actualizarhistoryañadiendo el arraysquaresactualizado como una nueva entrada en el historial. También quieres alternarxIsNext, tal como solía hacer Board:

Aquí,[...history, nextSquares]crea un nuevo array que contiene todos los elementos dehistory, seguidos pornextSquares. (Puedes leer la...historysintaxis de propagacióncomo "enumerar todos los elementos enhistory").)

Por ejemplo, sihistoryes[[null,null,null], ["X",null,null]] y nextSquareses["X",null,"O"], entonces el nuevo array[...history, nextSquares] será [[null,null,null], ["X",null,null], ["X",null,"O"]].

En este punto, has movido el estado para que viva en el componenteGame, y la interfaz de usuario debería funcionar completamente, tal como estaba antes de la refactorización. Así es como debería verse el código en este punto:

Mostrar los movimientos pasados

Como estás registrando el historial del juego de tres en raya, ahora puedes mostrar una lista de movimientos pasados al jugador.

Los elementos de React como<button>son objetos JavaScript normales; puedes pasarlos en tu aplicación. Para renderizar múltiples elementos en React, puedes usar un array de elementos de React.

Ya tienes un array de movimientoshistoryen el estado, así que ahora necesitas transformarlo en un array de elementos de React. En JavaScript, para transformar un array en otro, puedes usar elmétodo map de arrays:

Usarásmappara transformar tuhistoryde movimientos en elementos de React que representen botones en la pantalla, y mostrar una lista de botones para "saltar" a movimientos pasados. Vamos a aplicarmapsobre elhistoryen el componente Game:

Puedes ver cómo debería verse tu código a continuación. Ten en cuenta que deberías ver un error en la consola de herramientas de desarrollo que dice:

Solucionarás este error en la siguiente sección.

A medida que iteras sobre el arrayhistorydentro de la función que pasaste amap, el argumentosquarespasa por cada elemento dehistory, y el argumentomovepasa por cada índice del array:0,1,2, …. (En la mayoría de los casos, necesitarías los elementos reales del array, pero para renderizar una lista de movimientos solo necesitarás los índices).

Para cada movimiento en el historial del juego de tres en raya, creas un elemento de lista<li>que contiene un botón<button>. El botón tiene un manejadoronClickque llama a una función llamadajumpTo(que aún no has implementado).

Por ahora, deberías ver una lista de los movimientos que ocurrieron en el juego y un error en la consola de herramientas de desarrollo. Hablemos sobre lo que significa el error de "key".

Elegir una clave

Cuando renderizas una lista, React almacena cierta información sobre cada elemento de lista renderizado. Cuando actualizas una lista, React necesita determinar qué ha cambiado. Podrías haber añadido, eliminado, reordenado o actualizado los elementos de la lista.

Imagina la transición de

a

Además de los recuentos actualizados, un humano que lea esto probablemente diría que intercambiaste el orden de Alexa y Ben e insertaste a Claudia entre Alexa y Ben. Sin embargo, React es un programa de computadora y no sabe lo que pretendías, por lo que necesitas especificar una propiedadkeypara cada elemento de lista para diferenciar cada elemento de lista de sus hermanos. Si tus datos provinieran de una base de datos, los ID de base de datos de Alexa, Ben y Claudia podrían usarse como claves.

Cuando una lista se vuelve a renderizar, React toma la clave de cada elemento de lista y busca en los elementos de la lista anterior una clave coincidente. Si la lista actual tiene una clave que no existía antes, React crea un componente. Si a la lista actual le falta una clave que existía en la lista anterior, React destruye el componente anterior. Si dos claves coinciden, el componente correspondiente se mueve.

Las claves le indican a React la identidad de cada componente, lo que permite a React mantener el estado entre re-renderizados. Si la clave de un componente cambia, el componente se destruirá y se recreará con un nuevo estado.

keyes una propiedad especial y reservada en React. Cuando se crea un elemento, React extrae la propiedadkeyy almacena la clave directamente en el elemento devuelto. Aunquekeypueda parecer que se pasa como props, React usa automáticamentekeypara decidir qué componentes actualizar. No hay forma de que un componente pregunte quékeyespecificó su padre.

Se recomienda encarecidamente que asignes claves apropiadas siempre que construyas listas dinámicas.Si no tienes una clave apropiada, es posible que desees considerar reestructurar tus datos para que sí la tengas.

Si no se especifica ninguna clave, React informará de un error y usará el índice del array como clave por defecto. Usar el índice del array como clave es problemático cuando se intenta reordenar los elementos de una lista o insertar/eliminar elementos de la lista. Pasar explícitamentekey={i}silencia el error pero tiene los mismos problemas que los índices de array y no se recomienda en la mayoría de los casos.

Las claves no necesitan ser únicas globalmente; solo necesitan ser únicas entre componentes y sus hermanos.

Implementando el viaje en el tiempo

En el historial del juego de tres en raya, cada movimiento pasado tiene un ID único asociado: es el número secuencial del movimiento. Los movimientos nunca se reordenarán, eliminarán o insertarán en medio, por lo que es seguro usar el índice del movimiento como clave.

En la funciónGame, puedes añadir la clave como<li key={move}>, y si recargas el juego renderizado, el error de "key" de React debería desaparecer:

Antes de poder implementarjumpTo, necesitas que el componenteGamelleve un registro del paso que el usuario está viendo actualmente. Para hacer esto, define una nueva variable de estado llamadacurrentMove, con un valor predeterminado de0:

A continuación, actualiza la funciónjumpTodentro deGamepara actualizar esecurrentMove. También establecerásxIsNextentruesi el número al que estás cambiandocurrentMovees par.

Ahora harás dos cambios en la funciónhandlePlaydelGame, que se llama cuando haces clic en un cuadrado.

  • Si "retrocedes en el tiempo" y luego haces un nuevo movimiento desde ese punto, solo quieres mantener el historial hasta ese punto. En lugar de agregarnextSquaresdespués de todos los elementos (sintaxis de propagación...) en history, lo agregarás después de todos los elementos enhistory.slice(0, currentMove + 1)para que solo mantengas esa porción del historial antiguo.
  • Cada vez que se realiza un movimiento, necesitas actualizarcurrentMovepara que apunte a la entrada más reciente del historial.

Finalmente, modificarás el componenteGamepara renderizar el movimiento seleccionado actualmente, en lugar de siempre renderizar el movimiento final:

Si haces clic en cualquier paso del historial del juego, el tablero de tres en raya debería actualizarse inmediatamente para mostrar cómo se veía el tablero después de que ocurrió ese paso.

Limpieza final

Si observas el código muy de cerca, puedes notar quexIsNext === truecuandocurrentMovees par yxIsNext === falsecuandocurrentMovees impar. En otras palabras, si conoces el valor decurrentMove, entonces siempre puedes determinar cuál debería serxIsNext.

No hay razón para que almacenes ambos en el estado. De hecho, siempre intenta evitar estados redundantes. Simplificar lo que almacenas en el estado reduce errores y hace que tu código sea más fácil de entender. CambiaGamepara que no almacenexIsNextcomo una variable de estado separada y, en su lugar, lo determine en función delcurrentMove:

Ya no necesitas la declaración de estadoxIsNextni las llamadas asetXIsNext. Ahora, no hay posibilidad de quexIsNextse desincronice concurrentMove, incluso si cometes un error al codificar los componentes.

Conclusión

¡Felicidades! Has creado un juego de tres en raya que:

  • Te permite jugar al tres en raya,
  • Indica cuando un jugador ha ganado el juego,
  • Almacena el historial de un juego a medida que avanza,
  • Permite a los jugadores revisar el historial de un juego y ver versiones anteriores del tablero.

¡Buen trabajo! Esperamos que ahora sientas que tienes un conocimiento decente de cómo funciona React.

Consulta el resultado final aquí:

Si tienes tiempo extra o quieres practicar tus nuevas habilidades en React, aquí hay algunas ideas para mejoras que podrías hacer al juego de tres en raya, listadas en orden de dificultad creciente:

  1. Para el movimiento actual solamente, muestra "Estás en el movimiento #..." en lugar de un botón.
  2. ReescribeBoardpara usar dos bucles y crear los cuadrados en lugar de codificarlos manualmente.
  3. Añade un botón de alternancia que te permita ordenar los movimientos en orden ascendente o descendente.
  4. Cuando alguien gane, resalta los tres cuadrados que causaron la victoria (y cuando nadie gane, muestra un mensaje sobre el resultado siendo un empate).
  5. Muestra la ubicación de cada movimiento en el formato (fila, columna) en la lista del historial de movimientos.

A lo largo de este tutorial, has tocado conceptos de React que incluyen elementos, componentes, props y estado. Ahora que has visto cómo funcionan estos conceptos al construir un juego, consultaPensando en Reactpara ver cómo los mismos conceptos de React funcionan al construir la interfaz de usuario de una aplicación.