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.

Nota

Este tutorial fue diseñado para personas que prefieren aprender haciendo y quieren ver algo tangible de manera rápida. Si prefieres aprender cada concepto paso a paso, comienza con Describir la UI.

El tutorial se divide en varias secciones:

¿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>;
}

Nota

También puede seguir este tutorial utilizando su entorno de desarrollo local. Para hacer esto, necesitas:

  1. Instalar Node.js
  2. En la pestaña CodeSandbox que abrió anteriormente, presione el botón de la esquina superior izquierda para abrir el menú y luego seleccione Archivo > Exportar a ZIP en ese menú para descargar un archivo de los archivos localmente.
  3. Descomprima el archivo, luego abra la terminal y digite cd en el directorio que descomprimió
  4. Instale las dependencias con el comando npm install
  5. Ejecute el comando npm start para iniciar un servidor local y siga las indicaciones para ver el código que se ejecuta en un navegador

Si te quedas atascado, ¡no dejes que esto te detenga! Siga en línea en su lugar e intente una configuración local nuevamente más tarde.

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:

CodeSandbox con código de inicio
  1. La sección Files con una lista de archivos como App.js, index.js, styles.css y una carpeta llamada public
  2. El code editor donde verás el código fuente de tu archivo seleccionado
  3. 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:

Cuadrado lleno de x

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:

Console
/src/App.js: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

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:

dos cuadrados llenos de x

¡Excelente! Ahora solo necesitas copiar y pegar varias veces para agregar nueve cuadrados y…

nueve cuadrados llenos de x en una línea

¡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 divs 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 divs, tiene su tablero de tres en raya:

El tablero de Tres en linea esta lleno con números del 1 al 9

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>
    </>
  );
}

Nota

Psssst… ¡Eso es mucho para escribir! Está bien copiar y pegar el código de esta página. Sin embargo, si está preparado para un pequeño desafío, te recomendamos que solo copie el código que ha escrito manualmente al menos una vez.

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 divs del navegador, sus propios componentes Board y Square deben comenzar con una letra mayúscula.

Vamos a ver:

tablero lleno

¡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:

tablero lleno de valores

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:

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:

tablero de Tres en linea lleno de números del 1 al 9

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!".

Nota

Si está siguiendo este tutorial utilizando su entorno de desarrollo local, debe abrir la consola de su navegador. Por ejemplo, si usa el navegador Chrome, puede ver la Consola con el método abreviado de teclado Shift + Ctrl + J (en Windows/Linux) u Option + ⌘ + J (en macOS).

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”:

adicionando x al tablero

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:

React DevTools en CodeSandbox

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

Seleccionar componentes en la página con React DevTools

Nota

Para el desarrollo local, React DevTools está disponible como Chrome, Firefox, y Edge extensión del navegador. Después de instalarlo, la pestaña Componentes aparecerá en las Herramientas de desarrollo de su navegador para los sitios que usan React.

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:

tablero 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).

Nota

JavaScript admite cierres, 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ón handleClick puede leer el estado squares y llamar al método setSquares porque ambos están definidos dentro de la función Board.

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:

Console
Demasiados renderizados. React limita el número de renderizaciones para evitar 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:

llenado el tablero con X

¡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:

  1. Al hacer clic en el cuadrado superior izquierdo, se ejecuta la función que el botón recibió como accesorio onClick del Square. El componente Square recibió esa función como su accesorio onSquareClick del Board. El componente Board definió esa función directamente en el JSX. Llama a handleClick con un argumento de 0.
  2. handleClick usa el argumento (0) para actualizar el primer elemento de la matriz squares de null a X.
  3. El estado squares del componente Board se actualizó, por lo que Board y todos sus elementos secundarios se vuelven a renderizar. Esto hace que la prop value del componente Square con el índice 0 cambie de null a X.

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.

Nota

El atributo onClick del elemento DOM <button> tiene un significado especial para React porque es un componente integrado. Para componentes personalizados como Square, el nombre depende de usted. Podría dar cualquier nombre a la propiedad onSquareClick de Square o handleClick de Board, y el código funcionaría de la misma manera. En React, es convencional usar nombres on[Event] para accesorios que representan eventos y handle[Event] para las definiciones de funciones que manejan los eventos.

¿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:

O sobrescribiendo una X

¡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;
}

Nota

No importa si define calculateWinner antes o después del Board. Pongámoslo al final para que no tenga que desplazarse cada vez que edite sus componentes.

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 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 nulls.

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) en history, lo agregará después de todos los elementos en history.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:

  1. Solo para el movimiento actual, muestra “Estás en el movimiento #…” en lugar de un botón
  2. Vuelva a escribir Board para usar dos bucles para hacer los cuadrados en lugar de codificarlos.
  3. Agregue un botón de alternancia que le permita ordenar los movimientos en orden ascendente o descendente.
  4. 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).
  5. 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.