Tutorial: Tres en línea
En este tutorial construirás un pequeño juego de Tres en linea. Este tutorial no asume ningún conocimiento previo de React. Las técnicas que aprenderás en el tutorial son fundamentales para crear cualquier aplicación de React, y comprenderlas por completo te dará una comprensión profunda de React.
El tutorial se divide en varias secciones:
- Configuración para el tutorial le dará un punto de partida para seguir el tutorial.
- Descripción general te enseñará los fundamentos de React: componentes, props y estado.
- Completar el juego te enseñará las técnicas más comunes en el desarrollo de React.
- Agregar viajes en el tiempo le brindará una visión más profunda de las fortalezas únicas de React.
¿Qué estás construyendo?
En este tutorial, crearás un juego interactivo de Tres en línea con React.
Puedes ver cómo se verá cuando hayas terminado aquí:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Ir al movimiento #' + move; } else { description = 'Ir al inicio del juego'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
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 comprender React y su sintaxis.
Te recomendamos que consultes el juego de Tres en línea anterior 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 brinda un historial de todos los movimientos que se han producido en el juego y se actualiza a medida que avanza el juego.
Una vez que hayas jugado un poco con el juego Tres en línea 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, haga clic en Fork en 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 su navegador e inmediatamente ver cómo sus usuarios verán la aplicación que ha creado. La nueva pestaña debería mostrar un cuadrado vacío y el código de inicio para este tutorial.
export default function Square() { return <button className="square">X</button>; }
Descripción general
Ahora que está configurado, veamos una descripción general de React.
Inspeccionar el código de inicio
En CodeSandbox verás tres secciones principales:
- La sección Files con una lista de archivos como
App.js
,index.js
,styles.css
y una carpeta llamadapublic
- El code editor donde verás el código fuente de tu archivo seleccionado
- La sección browser donde verás cómo se mostrará el código que has escrito
El archivo App.js
debe seleccionarse en la sección Files. El contenido de ese archivo en el code editor debería ser:
export default function Square() {
return <button className="square">X</button>;
}
La sección del browser debería mostrar un cuadrado con una X como esta:
Ahora echemos un vistazo a los archivos en el código de inicio.
App.js
El código en App.js
crea un component. 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 representar, administrar y actualizar los elementos de la interfaz de usuario en su aplicación. Miremos el componente línea por línea para ver qué está pasando:
export default function Square() {
return <button className="square">X</button>;
}
La primera línea define una función llamada Square
. La palabra clave de JavaScript export
hace que esta función sea accesible fuera de este archivo. La palabra clave default
le dice a otros archivos que usan su código que es la función principal en su archivo.
export default function Square() {
return <button className="square">X</button>;
}
La segunda línea devuelve un botón. La palabra clave de JavaScript return
significa que lo que viene después se devuelve como un valor a la persona que llama a la función. <button>
es un elemento JSX. Un elemento JSX es una combinación de código JavaScript y etiquetas HTML que describe lo que le gustaría mostrar. className="square"
es una propiedad de botón o prop que le dice a CSS cómo diseñar el botón. X
es el texto que se muestra dentro del botón y </button>
cierra el elemento JSX para indicar que ningún contenido siguiente debe colocarse dentro del botón.
styles.css
Haga clic en el archivo etiquetado styles.css
en la sección Files de CodeSandbox. Este archivo define los estilos para su aplicación React. Los primeros dos selectores CSS (*
y body
) definen el estilo de grandes partes de su aplicación, mientras que el selector .square
define el estilo de cualquier componente donde la propiedad className
está establecida en square
. En su código, eso coincidiría con el botón de su componente Square en el archivo App.js
.
index.js
Haga clic en el archivo etiquetado index.js
en la sección Files de CodeSandbox. No editará este archivo durante el tutorial, pero es el puente entre el componente que creó en el archivo App.js
y el navegador web.
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import './styles.css';
import App from './App';
Las líneas 1-5 reúnen todas las piezas necesarias:
- React
- Biblioteca de React para hablar con los navegadores web (React DOM)
- los estilos para sus componentes
- el componente que creó en
App.js
.
El resto del archivo reúne todas las piezas e inyecta el producto final en index.html
en la carpeta public
.
Construyendo el tablero
Volvamos a App.js
. Aquí es donde pasará el resto del tutorial.
Actualmente, el tablero es solo un cuadrado, ¡pero necesitas nueve! Si solo intenta copiar y pegar su cuadrado para hacer dos cuadrados como este:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
Obtendrás este error:
<>...</>
?Los componentes de React deben devolver un solo elemento JSX y no múltiples elementos JSX adyacentes como dos botones. Para solucionar esto, puede usar fragmentos (<>
y </>
) para envolver múltiples elementos JSX adyacentes como este:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
Ahora deberías ver:
¡Excelente! Ahora solo necesitas copiar y pegar varias veces para agregar nueve cuadrados y…
¡Oh, no! Los cuadrados están todos en una sola línea, no en una cuadrícula como la que necesita para nuestro tablero. Para solucionar esto, deberá agrupar sus cuadrados en filas con div
s y agregar algunas clases de CSS. Mientras lo hace, le dará a cada cuadrado un número para asegurarse de saber dónde se muestra cada cuadrado.
En el archivo App.js
, actualice el componente Square
para que se vea así:
export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
El CSS definido en styles.css
diseña los divs con className
de board-row
. Ahora que ha agrupado sus componentes en filas con el estilo div
s, tiene su tablero de tres en raya:
Pero ahora tienes un problema. Su componente llamado Square
, en realidad ya no es un cuadrado. Arreglemos eso cambiando el nombre a Board
:
export default function Board() {
//...
}
En este punto, su código debería verse así:
export default function Board() { return ( <> <div className="board-row"> <button className="square">1</button> <button className="square">2</button> <button className="square">3</button> </div> <div className="board-row"> <button className="square">4</button> <button className="square">5</button> <button className="square">6</button> </div> <div className="board-row"> <button className="square">7</button> <button className="square">8</button> <button className="square">9</button> </div> </> ); }
Pasar datos a través de props
A continuación, querrá cambiar el valor de un cuadrado de vacío a “X” cuando el usuario haga clic en el cuadrado. Con la forma en que ha construido el tablero hasta ahora, necesitaría copiar y pegar el código que actualiza el cuadrado nueve veces (¡una vez por cada cuadrado que tenga)! En lugar de copiar y pegar, la arquitectura de componentes de React le permite crear un componente reutilizable para evitar el código duplicado desordenado.
Primero, va a copiar la línea que define su primer cuadrado (<button className="square">1</button>
) de su componente Board
en un nuevo componente Square
:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
Luego, actualizará el componente Board para renderizar ese componente Square
usando la sintaxis JSX:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Observe cómo, a diferencia de los div
s del navegador, sus propios componentes Board
y Square
deben comenzar con una letra mayúscula.
Vamos a ver:
¡Oh, no! Perdiste los cuadrados numerados que tenías antes. Ahora cada cuadrado dice “1”. Para arreglar esto, utilizará props para pasar el valor que debe tener cada cuadrado del componente principal (Board
) al componente secundario (Square
).
Actualice el componente Square
para leer la propiedad value
que pasará desde el Tablero
:
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
indica que al componente Square se le puede pasar un objeto llamado value
.
Ahora desea mostrar ese value
en lugar de 1
dentro de cada cuadrado. Intenta hacerlo así:
function Square({ value }) {
return <button className="square">value</button>;
}
Vaya, esto no es lo que querías:
Quería representar la variable de JavaScript llamada value
de su componente, no la palabra “valor”. Para “escapar a JavaScript” desde JSX, necesita llaves. Agregue llaves alrededor de value
en JSX así:
function Square({ value }) {
return <button className="square">{value}</button>;
}
Por ahora, deberías ver un tablero vacío:
Esto se debe a que el componente Board
aún no ha pasado el accesorio value
a cada componente Square
que representa. Para solucionarlo, agregará el complemento value
a cada componente Square
representado por el componente Board
:
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}
Ahora debería ver una cuadrícula de números nuevamente:
Su código actualizado debería verse así:
function Square({ value }) { return <button className="square">{value}</button>; } export default function Board() { return ( <> <div className="board-row"> <Square value="1" /> <Square value="2" /> <Square value="3" /> </div> <div className="board-row"> <Square value="4" /> <Square value="5" /> <Square value="6" /> </div> <div className="board-row"> <Square value="7" /> <Square value="8" /> <Square value="9" /> </div> </> ); }
Haciendo un componente interactivo.
Rellenemos el componente Square
con una X
al hacer clic en él. Declare una función llamada handleClick
dentro del Square
. Luego, agregue onClick
a los accesorios del elemento botón JSX devuelto por el componente Square
:
function Square({ value }) {
function handleClick() {
console.log('¡ha hecho clic!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Si hace clic en un cuadrado ahora, debería ver un registro que dice "¡ha hecho clic!"
en la pestaña Consola en la parte inferior de la sección Navegador en CodeSandbox. Al hacer clic en el cuadrado más de una vez, se 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á un contador incremental al lado de su primer registro "¡haciendo clic!"
.
Como siguiente paso, desea que el componente Square “recuerde” que se hizo clic y lo rellene con una marca “X”. Para “recordar” cosas, los componentes usan estado.
React proporciona una función especial llamada useState
que puede llamar desde su componente para permitirle “recordar” cosas. Almacenemos el valor actual del Square
en estado, y cambiémoslo cuando se haga clic en Square
.
Importe useState
en la parte superior del archivo. Elimine la propiedad value
del componente Square. En su lugar, agregue una nueva línea al comienzo del componente Square
que llame a useState
. Haz que devuelva una variable de estado llamada value
:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
almacena el valor y setValue
es una función que se puede usar para cambiar el valor. El null
pasado a useState
se usa como valor inicial para esta variable de estado, por lo que value
aquí comienza igual a null
.
Dado que el componente Square
ya no acepta props, eliminará el accesorio value
de los nueve componentes Square creados por el componente Board:
// ...
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
Ahora cambiará Square
para mostrar una “X” cuando se haga clic. Reemplace el controlador de eventos console.log("clicked!");
con setValue('X');
. Ahora su componente Square
se ve así:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Al llamar a esta función set
desde un controlador onClick
, le estás diciendo a React que vuelva a renderizar ese Square
cada vez que se haga clic en su <button>
. Después de la actualización, el value
del Square
será 'X'
, por lo que verás la “X” en el tablero de juego.
Si hace clic en cualquier cuadrado, debería aparecer una “X”:
Tenga en cuenta que cada Cuadrado tiene su propio estado: el value
almacenado en cada Square es completamente independiente de los demás. Cuando llamas a una función set
en un componente, React también actualiza automáticamente los componentes secundarios dentro de él.
Después de realizar los cambios anteriores, su código se verá así:
import { useState } from 'react'; function Square() { const [value, setValue] = useState(null); function handleClick() { setValue('X'); } return ( <button className="square" onClick={handleClick} > {value} </button> ); } export default function Board() { return ( <> <div className="board-row"> <Square /> <Square /> <Square /> </div> <div className="board-row"> <Square /> <Square /> <Square /> </div> <div className="board-row"> <Square /> <Square /> <Square /> </div> </> ); }
React Developer Tools
React DevTools le permite verificar los accesorios y el estado de sus componentes React. Puede encontrar la pestaña React DevTools en la parte inferior de la sección navegador en CodeSandbox:
Para inspeccionar un componente en particular en la pantalla, use el botón en la esquina superior izquierda de React DevTools:
Completando el juego
En este punto, tienes todos los componentes básicos para tu juego de tres en raya. Para tener un juego completo, ahora necesita alternar la colocación de “X” y “O” en el tablero, y necesita una forma de determinar un ganador.
Levantando el estado
Actualmente, cada componente Square
mantiene una parte del estado del juego. Para comprobar si hay un ganador en un juego de tres en raya, el Board
necesitaría saber de alguna manera el estado de cada uno de los 9 componentes del Square
.
¿Cómo abordarías eso? Al principio, puede suponer que el Board
necesita “preguntar” a cada Square
por el estado de ese Square
. Aunque este enfoque es técnicamente posible en React, lo desaconsejamos porque el código se vuelve difícil de entender, susceptible a errores y difícil de refactorizar. En cambio, el mejor enfoque es almacenar el estado del juego en el componente Board
principal en lugar de en cada Square
. El componente Board
puede decirle a cada ‘Cuadrado’ qué mostrar al pasar un accesorio, como lo hizo cuando pasó un número a cada Cuadrado.
Para recopilar datos de varios elementos secundarios o para que dos componentes secundarios se comuniquen entre sí, declare el estado compartido en su componente principal. El componente padre puede devolver ese estado a los hijos a través de accesorios. Esto mantiene los componentes secundarios sincronizados entre sí y con el componente principal.
Elevar el estado a un componente principal es común cuando se refactorizan los componentes de React.
Aprovechemos esta oportunidad para probarlo. Edite el componente Board
para que declare una variable de estado llamada Square
que por defecto sea una matriz de 9 valores nulos correspondientes a los 9 cuadrados:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
crea una matriz con nueve elementos y establece cada uno de ellos en null
. La llamada useState()
a su alrededor declara una variable de estado squares
que inicialmente se establece en esa matriz. Cada entrada en la matriz corresponde al valor de un cuadrado. Cuando llene el tablero más tarde, la matriz de “cuadrados” se verá así:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
Ahora su componente Board
necesita pasar el accesorio value
a cada uno de los componentes Square
que representa:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
A continuación, editará el componente Square
para recibir el accesorio value
del componente Board. Esto requerirá eliminar el propio seguimiento con estado del value
del componente Square y la propiedad onClick
del botón:
function Square({value}) {
return <button className="square">{value}</button>;
}
En este punto, deberías ver un tablero de Tres en línea vacío:
Y tu código debería verse así:
import { useState } from 'react'; function Square({ value }) { return <button className="square">{value}</button>; } export default function Board() { const [squares, setSquares] = useState(Array(9).fill(null)); return ( <> <div className="board-row"> <Square value={squares[0]} /> <Square value={squares[1]} /> <Square value={squares[2]} /> </div> <div className="board-row"> <Square value={squares[3]} /> <Square value={squares[4]} /> <Square value={squares[5]} /> </div> <div className="board-row"> <Square value={squares[6]} /> <Square value={squares[7]} /> <Square value={squares[8]} /> </div> </> ); }
Cada Cuadrado ahora recibirá un accesorio de value
que será 'X'
, 'O'
, o nul
para los cuadrados vacíos.
A continuación, debe cambiar lo que sucede cuando se hace clic en un Square
. El componente Board
ahora mantiene qué casillas están llenas. Necesitarás crear una forma para que Square
actualice el estado de Board
. Dado que el estado es privado para un componente que lo define, no puede actualizar el estado de Board
directamente desde Square
.
En su lugar, pasará una función del componente Board
al componente Square
, y Square
llamará a esa función cuando se haga clic en un cuadrado. Comenzará con la función que llamará el componente Square
cuando se haga clic en él. Llamarás a esa función onSquareClick
:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
A continuación, agregará la función onSquareClick
a los accesorios del componente Square
:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
Ahora conectará el accesorio onSquareClick
a una función en el componente Board
que llamará handleClick
. Para conectar onSquareClick
a handleClick
, pasará una función a la propiedad onSquareClick
del primer componente Square
:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
Por último, definirá la función handleClick
dentro del componente Board para actualizar la matriz squares
que contiene el estado de su tablero:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
La función handleClick
crea una copia de la matriz squares
(nextSquares
) con el método JavaScript slice()
Array. Luego, handleClick
actualiza la matriz nextSquares
para agregar X
al primer cuadrado (índice [0]
).
Llamar a la función setSquares
le permite a React saber que el estado del componente ha cambiado. Esto activará una nueva representación de los componentes que usan el estado de squares
(Boards
), así como sus componentes secundarios (los componentes Squares
que forman el tablero).
Ahora puede agregar X al tablero… pero solo al cuadrado superior izquierdo. Su función handleClick
está codificada para actualizar el índice del cuadrado superior izquierdo (0
). Actualicemos handleClick
para poder actualizar cualquier cuadrado. Agregue un argumento i
a la función handleClick
que toma el índice del cuadrado que debe actualizarse:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
Ahora hay un nuevo problema!
Intente configurar el complemento onSquareClick
del cuadrado para que sea handleClick(0)
directamente en el JSX de esta manera:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
La llamada handleClick(0)
será parte de la representación del componente de la placa. Debido a que handleClick(0)
altera el estado del componente del tablero llamando a setSquares
, todo el componente del tablero se volverá a renderizar. Pero handleClick(0)
ahora es parte de la representación del componente de la placa, por lo que ha creado un bucle infinito:
¿Por qué este problema no sucedió antes?
Cuando estabas pasando onSquareClick={handleClick}
, estabas pasando la función handleClick
como apoyo. ¡No lo estabas llamando! Pero ahora está llamando a esa función de inmediato, observe los paréntesis en handleClick(0)
, y es por eso que se ejecuta demasiado pronto. ¡No quieres llamar a handleClick
hasta que el usuario haga clic!
Para solucionar esto, puede crear una función como handleFirstSquareClick
que llame a handleClick(0)
, una función como handleSecondSquareClick
que llame a handleClick(1)
, y así sucesivamente. En lugar de llamarlas, pasaría estas funciones como accesorios como onSquareClick={handleFirstSquareClick}
. Esto resolvería el bucle infinito.
Sin embargo, definir nueve funciones diferentes y darles un nombre a cada una de ellas es demasiado detallado. En cambio, hagamos esto:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
Observe la nueva sintaxis () =>
. Aquí, () => handleClick(0)
es una función de 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 a handleClick(0)
.
Ahora necesita actualizar los otros ocho cuadrados para llamar a handleClick
desde las funciones de flecha que pasa. Asegúrese de que el argumento para cada llamada handleClick
corresponda al índice del cuadrado correcto:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
};
Ahora puede volver a agregar X a cualquier casilla del tablero haciendo clic en ellos:
¡Pero esta vez toda la gestión estatal está a cargo del componente ‘Board’!
Así es como debería verse tu código:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { const nextSquares = squares.slice(); nextSquares[i] = 'X'; setSquares(nextSquares); } return ( <> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); }
Ahora que su manejo de estado está en el componente Board
, el componente padre Board
pasa accesorios a los componentes hijos Square
para que puedan mostrarse correctamente. Al hacer clic en un ‘Cuadrado’, el componente secundario ‘Cuadrado’ ahora le pide al componente principal ‘Tablero’ que actualice el estado del tablero. Cuando el estado de Board
cambia, tanto el componente Board
como todos los componentes secundarios Square
se vuelven a renderizar automáticamente. Mantener el estado de todos los cuadrados en el componente “Tablero” le permitirá determinar el ganador en el futuro.
Recapitulemos lo que sucede cuando un usuario hace clic en el cuadrado superior izquierdo de su tablero para agregarle una X
:
- Al hacer clic en el cuadrado superior izquierdo, se ejecuta la función que el
botón
recibió como accesorioonClick
delSquare
. El componenteSquare
recibió esa función como su accesorioonSquareClick
delBoard
. El componenteBoard
definió esa función directamente en el JSX. Llama ahandleClick
con un argumento de0
. handleClick
usa el argumento (0
) para actualizar el primer elemento de la matrizsquares
denull
aX
.- El estado
squares
del componenteBoard
se actualizó, por lo queBoard
y todos sus elementos secundarios se vuelven a renderizar. Esto hace que la propvalue
del componenteSquare
con el índice0
cambie denull
aX
.
Al final, el usuario ve que el cuadrado superior izquierdo ha pasado de estar vacío a tener una X
después de hacer clic en él.
¿Por qué es importante la inmutabilidad?
Observe cómo en handleClick
, llama a .slice()
para crear una copia de la matriz squares
en lugar de modificar la matriz existente. Para explicar por qué, necesitamos discutir la inmutabilidad y por qué es importante aprender la inmutabilidad.
En general, hay dos enfoques para cambiar los datos. El primer enfoque es mutar los datos cambiando directamente los valores de los datos. El segundo enfoque es reemplazar los datos con una nueva copia que tenga los cambios deseados. Así es como se vería si mutaras la matriz squares
:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];
Y así es como se vería si cambiaras los datos sin mutar la matriz squares
:
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
El resultado final es el mismo, pero al no mutar (cambiar los datos subyacentes) directamente, obtiene varios beneficios.
La inmutabilidad hace que las características complejas sean mucho más fáciles de implementar. Más adelante en este tutorial, implementará una función de “viaje en el tiempo” que le permitirá revisar el historial del juego y “retroceder” a movimientos anteriores. 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 le permite mantener intactas las versiones anteriores de los datos y reutilizarlas (o restablecerlas) más adelante.
También hay otro beneficio de la inmutabilidad. De forma predeterminada, todos los componentes secundarios se vuelven a renderizar automáticamente cuando cambia el estado de un componente principal. Esto incluye incluso los componentes secundarios que no se vieron afectados por el cambio. Aunque el usuario no nota la renderización en sí misma (¡no debe tratar de evitarla de forma activa!), es posible que desee omitir la renderización de una parte del árbol que claramente no se vio afectada por razones de rendimiento. La inmutabilidad hace que sea muy barato para los componentes comparar si sus datos han cambiado o no. Puede obtener más información sobre cómo React elige cuándo volver a renderizar un componente en la documentación de referencia de la API memo
.
Tomando 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á que el primer movimiento sea “X” de forma predeterminada. Hagamos un seguimiento de esto agregando otra parte del estado al componente Tablero:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
Cada vez que un jugador se mueve, xIsNext
(un valor booleano) se invertirá para determinar qué jugador es el siguiente y se guardará el estado del juego. Actualizará la función handleClick
de Board
para cambiar el valor de xIsNext
:
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
return (
//...
);
}
Ahora, al hacer clic en diferentes cuadrados, se alternarán entre X
y O
, ¡como deberían!
Pero espera, hay un problema. Intente hacer clic en el mismo cuadrado varias veces:
¡La X
se sobrescribe con una O
! Si bien esto agregaría un giro muy interesante al juego, por ahora nos apegaremos a las reglas originales.
Cuando marcas un cuadrado con una X
o una O
, no estás comprobando primero si el cuadrado ya tiene un valor X
u O
. Puedes arreglar esto regresando temprano. Verificará si el cuadrado ya tiene una X
o una O
. Si el cuadrado ya está lleno, generará un return
en la función handleClick
, antes de que intente actualizar el estado del tablero.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
¡Ahora solo puedes agregar X
u O
a los cuadrados vacíos! Así es como debería verse su código en este punto:
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [xIsNext, setXIsNext] = useState(true); const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { if (squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } setSquares(nextSquares); setXIsNext(!xIsNext); } return ( <> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); }
Declaring a winner
Ahora que muestra el turno de qué jugador es el siguiente, también debe mostrar cuándo se gana el juego y no hay más turnos para hacer. Para hacer esto, agregará una función de ayuda llamada calculateWinner
que toma una matriz de 9 cuadrados, busca un ganador y devuelve 'X'
, 'O'
o null
según corresponda. No se preocupe demasiado por la función calculateWinner
; no es específico de React:
export default function Board() {
//...
}
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
Llamarás a calculateWinner(squares)
en la función handleClick
del componente Board
para comprobar si un jugador ha ganado. Puede realizar esta verificación al mismo tiempo que verifica si un usuario ha hecho clic en un cuadrado que ya tiene una X
o una O
. Nos gustaría volver temprano en ambos casos:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
Para que los jugadores sepan cuándo termina el juego, puede mostrar texto como “Ganador: X” o “Ganador: O”. Para hacerlo, agregará una sección status
al componente Board
. El estado mostrará el ganador si el juego ha terminado y si el juego está en curso, se mostrará el turno del siguiente jugador:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Ganador: " + winner;
} else {
status = "Siguiente jugador: " + (xIsNext ? "X" : "O");
}
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
¡Felicidades! Ahora tienes un juego de Tres en linea que funciona. Y acabas de aprender los conceptos básicos de React también. Así que tú eres el verdadero ganador aquí. Así es como debería verse el código:
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } export default function Board() { const [xIsNext, setXIsNext] = useState(true); const [squares, setSquares] = useState(Array(9).fill(null)); function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } setSquares(nextSquares); setXIsNext(!xIsNext); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Adding time travel
As a final exercise, let’s make it possible to “go back in time” to the previous moves in the game.
Almacenar un historial de movimientos
Si mutara la matriz squares
, implementar el viaje en el tiempo sería muy difícil.
Sin embargo, usó slice()
para crear una nueva copia de la matriz squares
después de cada movimiento, y la trató como inmutable. Esto le permitirá almacenar todas las versiones anteriores de la matriz squares
y navegar entre los giros que ya han ocurrido.
Almacenará las matrices anteriores de ’squares
en otra matriz llamada history
, que almacenará como una nueva variable de estado. La matriz history
representa todos los estados del tablero, desde el primero hasta el último movimiento, y tiene una forma como esta:
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
Levantando el estado, otra vez
Ahora escribirá un nuevo componente de nivel superior llamado Game
para mostrar una lista de movimientos anteriores. Ahí es donde colocarás el estado de history
que contiene todo el historial del juego.
Colocar el estado history
en el componente Game
le permitirá eliminar el estado squares
de su componente hijo Board
. Al igual que “levantó el estado” del componente Square
al componente Board
, ahora lo elevará del Board
al componente Game
de nivel superior. Esto le da al componente Game
control total sobre los datos del Board
y le permite instruir al Game
para renderizar los turnos anteriores del history
.
Primero, agregue un componente Game
con export default
. Haga que represente el componente Board
dentro de algún marcado:
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
Tenga en cuenta que está eliminando las palabras clave export default
antes de la declaración function Board() {
y agregándolas antes de la declaración function Game() {
. Esto le dice a su archivo index.js
que use el componente Game
como el componente de nivel superior en lugar de su componente Board
. Los ‘div’ adicionales devueltos por el componente ‘Juego’ están dejando espacio para la información del juego que agregará al tablero más adelante.
Agregue algún estado al componente Game
para rastrear qué jugador es el siguiente y el historial de movimientos:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
Observe cómo [Array(9).fill(null)]
es una matriz con un solo elemento, que a su vez es una matriz de 9 null
s.
Para renderizar los cuadrados para el movimiento actual, querrás leer la matriz de los últimos cuadrados del history
. No necesita useState
para esto; ya tiene suficiente información para calcularlo durante el renderizado:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
A continuación, cree una función handlePlay
dentro del componente Game
que será llamada por el componente Board
para actualizar el juego. Pase xIsNext
, currentSquares
y handlePlay
como accesorios al componente Board
:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
// TODO
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}
Hagamos que el componente Board
esté completamente controlado por los accesorios que recibe. Cambie el componente Board
para que tome tres accesorios: xIsNext
, squares
y una nueva función onPlay
que Board
puede llamar con la matriz de cuadrados actualizada cada vez que un jugador hace un movimiento. A continuación, elimine las dos primeras líneas de la función Board
que llama a useState
:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
Ahora reemplazará las llamadas setSquares
y setXIsNext
en handleClick
en el componente Board
con una sola llamada a su nueva función onPlay
para que el componente Game
pueda actualizar Board
cuando el usuario hace clic en un cuadrado:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
El componente Board
está totalmente controlado por los accesorios que le pasa el componente Game
. Necesitas implementar la función handlePlay
en el componente Game
para que el juego vuelva a funcionar.
¿Qué debería hacer handlePlay
cuando se llama? Recuerda que Board solía llamar a setSquares
con una matriz actualizada; ahora pasa la matriz squares
actualizada a onPlay
.
La función handlePlay
necesita actualizar el estado de Game
para activar una nueva representación, pero ya no tienes una función setSquares
a la que puedas llamar; ahora estás usando el estado history
variable para almacenar esta información. Querrás actualizar el history
agregando la matriz squares
actualizada como una nueva entrada en el historial. También desea alternar xIsNext
, tal como solía hacer Board:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
Aquí, [...history, nextSquares]
crea una nueva matriz que contiene todos los elementos en history
, seguido de nextSquares
. (Puedes leer el ...history
sintaxis extendida como “enumerar todos los elementos en history
”.)
Por ejemplo, si history
es igual a [[null,null,null], ["X",null,null]]
y nextSquares
es igual a ["X",null,"O"]
, entonces el nuevo arreglo[...history, nextSquares]
será [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
En este punto, ha movido el estado para vivir en el componente Game
, y la interfaz de usuario debería estar funcionando completamente, tal como estaba antes de la refactorización. Así es como debería verse el código en este punto:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente Jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{/*TODO*/}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Mostrando los movimientos anteriores
Dado que está grabando el historial del juego de tres en raya, ahora puede mostrárselo al jugador como una lista de movimientos anteriores.
Los elementos de React como <button>
son objetos regulares de JavaScript; puede pasarlos en su aplicación. Para representar varios elementos en React, puede usar una matriz de elementos de React.
Ya tienes una matriz de movimientos history
en el estado, por lo que ahora necesitas transformarla en una matriz de elementos React. En JavaScript, para transformar una matriz en otra, puede usar el método de array map
:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
Usará map
para transformar su history
de movimientos en elementos de React que representan botones en la pantalla, y mostrará una lista de botones para “saltar” a movimientos anteriores. Hagamos un ‘mapa’ sobre la ‘historia’ en el componente Juego:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {
// TODO
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Ir al movimiento #' + move;
} else {
description = 'Ir al inicio del juego';
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
Puede ver cómo debería verse su código a continuación. Tenga en cuenta que debería ver un error en la consola de herramientas para desarrolladores que dice: Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
Resolverás este error en la siguiente sección.
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } function jumpTo(nextMove) { // TODO } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Ir al movimiento #' + move; } else { description = 'Ir al inicio del juego'; } return ( <li> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
A medida que itera a través de la matriz historia
dentro de la función que pasó a map
, el argumento squares
pasa por cada elemento de history
, y el argumento move
pasa por cada índice de matriz: 0
, `1 ’, ‘2’, …. (En la mayoría de los casos, necesitaría los elementos de la matriz real, pero en este caso no usa “cuadrados” para representar una lista de movimientos).
Para cada movimiento en el historial del juego de tres en raya, crea un elemento de lista <li>
que contiene un botón <button>
. El botón tiene un controlador onClick
que llama a una función llamada jumpTo
(que aún no has implementado).
Por ahora, debería ver una lista de los movimientos que ocurrieron en el juego y un error en la consola de herramientas del desarrollador.
Analicemos qué significa el error “key”.
Elegir una llave
Cuando renderiza una lista, React almacena cierta información sobre cada elemento de la lista renderizada. Cuando actualiza una lista, React necesita determinar qué ha cambiado. Podría haber agregado, eliminado, reorganizado o actualizado los elementos de la lista.
Imagina la transición de
<li>Alexa: Quedan 7 tareas</li>
<li>Ben: Quedan 5 tareas</li>
hacia
<li>Ben: Quedan 9 tareas pendientes</li>
<li>Claudia: Quedan 8 tareas pendientes</li>
<li>Alexa: Quedan 5 tareas pendientes</li>
Además de los recuentos actualizados, una persona que lea esto probablemente diría que intercambiaste los pedidos de Alexa y Ben e insertaste a Claudia entre Alexa y Ben. Sin embargo, React es un programa de computadora y no puede saber lo que pretendía, por lo que debe especificar una propiedad key para cada elemento de la lista para diferenciar cada elemento de la lista de sus hermanos. Si estaba mostrando datos de una base de datos, los ID de la base de datos de Alexa, Ben y Claudia podrían usarse como claves.
<li key={user.id}>
{user.name}: {user.taskCount} tareas pendientes
</li>
Cuando se vuelve a representar una lista, React toma la clave de cada elemento de la 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, se mueve el componente correspondiente.
Las claves informan a React sobre la identidad de cada componente, lo que permite a React mantener el estado entre renderizaciones. Si la clave de un componente cambia, el componente se destruirá y se volverá a crear con un nuevo estado.
key
es una propiedad especial y reservada en React. Cuando se crea un elemento, React extrae la propiedad key
y almacena la clave directamente en el elemento devuelto. Aunque puede parecer que key
se pasa como accesorios, React usa automáticamente key
para decidir qué componentes actualizar. No hay forma de que un componente pregunte qué key
especificó su padre.
Se recomienda encarecidamente que asigne las claves adecuadas cada vez que cree listas dinámicas. Si no tiene una clave adecuada, puede considerar reestructurar sus datos para que la tenga.
Si no se especifica ninguna clave, React informará un error y utilizará el índice de matriz como clave de forma predeterminada. El uso del índice de la matriz como clave es problemático cuando se intenta reordenar los elementos de una lista o al insertar/eliminar elementos de la lista. Pasar explícitamente key={i}
silencia el error pero tiene los mismos problemas que los índices de matriz y no se recomienda en la mayoría de los casos.
Las claves no necesitan ser globalmente únicas; solo necesitan ser únicos entre los componentes y sus hermanos.
Implementación de viajes en el tiempo
En la historia del juego de tres en raya, cada movimiento pasado tiene una identificación única asociada: es el número secuencial del movimiento. Los movimientos nunca se reordenarán, eliminarán o insertarán en el medio, por lo que es seguro usar el índice de movimiento como clave.
En la función Game
, puedes agregar la clave como <li key={move}>
, y si vuelves a cargar el juego renderizado, el error de “clave” de React debería desaparecer:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const currentSquares = history[history.length - 1]; function handlePlay(nextSquares) { setHistory([...history, nextSquares]); setXIsNext(!xIsNext); } function jumpTo(nextMove) { // TODO } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Ir hacia la jugada #' + move; } else { description = 'Ir al inicio del juego'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Antes de que pueda implementar jumpTo
, necesita el componente Game
para realizar un seguimiento de qué paso está viendo actualmente el usuario. Para hacer esto, defina una nueva variable de estado llamada currentMove
, por defecto a 0
:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[history.length - 1];
//...
}
A continuación, actualice la función jumpTo
dentro de Game
para actualizar ese currentMove
. También establecerá xIsNext
en true
si el número al que está cambiando currentMove
es par.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
Ahora harás dos cambios en la función handlePlay
del Game
que se llama cuando haces clic en un cuadrado.
- Si “retrocedes en el tiempo” y luego haces un nuevo movimiento desde ese punto, solo querrás mantener el historial hasta ese punto. En lugar de agregar
nextSquares
después de todos los elementos (...
sintaxis extendida) enhistory
, lo agregará después de todos los elementos enhistory.slice (0, currentMove + 1)
para que solo manteniendo esa parte de la historia antigua. - Cada vez que se realiza un movimiento, debe actualizar
currentMove
para que apunte a la última entrada del historial.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
Finalmente, modificará el componente Game
para representar el movimiento seleccionado actualmente, en lugar de representar siempre el movimiento final:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[currentMove];
// ...
}
Si hace clic en cualquier paso en el 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 ocurriera ese paso.
import { useState } from 'react'; function Square({value, onSquareClick}) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente Jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [xIsNext, setXIsNext] = useState(true); const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); setXIsNext(!xIsNext); } function jumpTo(nextMove) { setCurrentMove(nextMove); setXIsNext(nextMove % 2 === 0); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Ir hacia la jugada #' + move; } else { description = 'Ir al inicio del juego'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Limpieza final
Si miras el código muy de cerca, puedes notar que xIsNext === true
cuando currentMove
es par y xIsNext === false
cuando currentMove
es impar. En otras palabras, si conoce el valor de movimientoActual
, entonces siempre puede averiguar cuál debería ser xIsNext
.
No hay ninguna razón para que almacene ambos en el estado. De hecho, trate siempre de evitar el estado redundante. Simplificar lo que almacena en el estado ayuda a reducir los errores y hace que su código sea más fácil de entender. Cambie Game
para que ya no almacene xIsNext
como una variable de estado separada y, en su lugar, lo descubra en función de currentMove
:
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// ...
}
Ya no necesita la declaración de estado xIsNext
ni las llamadas a setXIsNext
. Ahora, no hay posibilidad de que xIsNext
no esté sincronizado con currentMove
, incluso si comete un error al codificar los componentes.
Terminando
¡Felicidades! Has creado un juego de Tres en linea que:
- Te permite jugar Tres en linea,
- Indica cuando un jugador ha ganado el juego,
- Almacena el historial de un juego a medida que avanza un juego,
- Permite a los jugadores revisar el historial de un juego y ver versiones anteriores del tablero de un juego.
¡Buen trabajo! Esperamos que ahora sienta que tiene una comprensión decente de cómo funciona React.
Mira el resultado final aquí:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } function Board({ xIsNext, squares, onPlay }) { function handleClick(i) { if (calculateWinner(squares) || squares[i]) { return; } const nextSquares = squares.slice(); if (xIsNext) { nextSquares[i] = 'X'; } else { nextSquares[i] = 'O'; } onPlay(nextSquares); } const winner = calculateWinner(squares); let status; if (winner) { status = 'Ganador: ' + winner; } else { status = 'Siguiente jugador: ' + (xIsNext ? 'X' : 'O'); } return ( <> <div className="status">{status}</div> <div className="board-row"> <Square value={squares[0]} onSquareClick={() => handleClick(0)} /> <Square value={squares[1]} onSquareClick={() => handleClick(1)} /> <Square value={squares[2]} onSquareClick={() => handleClick(2)} /> </div> <div className="board-row"> <Square value={squares[3]} onSquareClick={() => handleClick(3)} /> <Square value={squares[4]} onSquareClick={() => handleClick(4)} /> <Square value={squares[5]} onSquareClick={() => handleClick(5)} /> </div> <div className="board-row"> <Square value={squares[6]} onSquareClick={() => handleClick(6)} /> <Square value={squares[7]} onSquareClick={() => handleClick(7)} /> <Square value={squares[8]} onSquareClick={() => handleClick(8)} /> </div> </> ); } export default function Game() { const [history, setHistory] = useState([Array(9).fill(null)]); const [currentMove, setCurrentMove] = useState(0); const xIsNext = currentMove % 2 === 0; const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); } function jumpTo(nextMove) { setCurrentMove(nextMove); } const moves = history.map((squares, move) => { let description; if (move > 0) { description = 'Ir hacia la jugada #' + move; } else { description = 'Ir al inicio del juego'; } return ( <li key={move}> <button onClick={() => jumpTo(move)}>{description}</button> </li> ); }); return ( <div className="game"> <div className="game-board"> <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /> </div> <div className="game-info"> <ol>{moves}</ol> </div> </div> ); } function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Si tiene tiempo extra o quiere practicar sus nuevas habilidades de React, aquí hay algunas ideas de mejoras que podría hacer al juego de tres en raya, enumeradas en orden de dificultad creciente:
- Solo para el movimiento actual, muestra “Estás en el movimiento #…” en lugar de un botón
- Vuelva a escribir
Board
para usar dos bucles para hacer los cuadrados en lugar de codificarlos. - Agregue un botón de alternancia que le permita ordenar los movimientos en orden ascendente o descendente.
- Cuando alguien gane, resalte los tres cuadrados que causaron la victoria (y cuando nadie gane, muestre un mensaje indicando que el resultado fue un empate).
- Muestre la ubicación de cada movimiento en el formato (columna, fila) en la lista del historial de movimientos.
A lo largo de este tutorial, ha tocado los conceptos de React, incluidos los elementos, los componentes, los accesorios y el estado. Ahora que ha visto cómo funcionan estos conceptos al crear un juego, consulte Pensando en React para ver cómo funcionan los mismos conceptos de React al crear la interfaz de usuario de una aplicación.