实现你的第一个React项目

初识React

什么是React

React是一个用于构建用户界面的JavaScript库,由Facebook开发和维护。它允许开发者通过组合简单的组件来构建复杂的用户界面。

为什么选择React

  • 组件化:React的设计思想是将UI分解成独立的、可复用的组件。
  • 高效:React使用虚拟DOM来减少直接操作DOM的次数,从而提高性能。
  • 社区支持:React有一个庞大的开发者社区和丰富的生态系统。

开始你的第一个React项目

我们可以利用vite 来创建一个react项目(Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。)

sql 复制代码
npm create vite@latest

用你的编译器打开项目,并且在终端执行命令安装依赖,再用npm run dev 来启动项目

arduino 复制代码
npm intall
npm run dev

打开链接可以看到网页

然后删除App.css和App.jsx的内容,准备编写我们自己的项目

在编写之前了解一下js与jsx文件

特点 JS 文件 JSX 文件
扩展名 .js .jsx
语法 纯 JavaScript JavaScript + JSX
用途 通用 JavaScript 逻辑 React 组件的 UI 结构
是否需要编译 通常不需要(除非使用 ES6+) 需要编译成纯 JavaScript
支持 HTML 样式 不支持 支持(通过 JSX 语法)

开始

App.jsx

将App.jsx里的文件修改为:

javascript 复制代码
export default function App() {
    return <div></div>;
}

App.jsx 返回了一个空组件App,后续我们会在这里加入更多我们写的组件

注意,这里的样式并不是React自带的,所以我们需要自己定义并引入,具体见下

index.css

该文件定义了 React 应用的大体样式。

为和官方教程样式一致,将index.css内容先替换为

css 复制代码
* {
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
  margin: 20px;
  padding: 0;
}

h1 {
  margin-top: 0;
  font-size: 22px;
}

h2 {
  margin-top: 0;
  font-size: 20px;
}

h3 {
  margin-top: 0;
  font-size: 18px;
}

h4 {
  margin-top: 0;
  font-size: 16px;
}

h5 {
  margin-top: 0;
  font-size: 14px;
}

h6 {
  margin-top: 0;
  font-size: 12px;
}

code {
  font-size: 1.2em;
}

ul {
  padding-inline-start: 20px;
}
main.jsx

在本教程中我们不会编辑此文件,但它是 App.js 文件中创建的组件与 Web 浏览器之间的桥梁。

javascript 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

第 1-4 行将所有必要的部分组合在一起:

  • React
  • React 与 Web 浏览器对话的库(React DOM)
  • 组件的样式
  • App.js 里面创建的组件

其他文件将它们组合在一起,并将最终成果注入 public 文件夹里面的 index.html 中。

构建棋盘

首先,创建一个存放组件的文件夹,再创建一个Board文件夹,存放Board.jsx和样式文件board.css

先在Board.jsx中创建一个方块组件

javascript 复制代码
export default function Board() {

  return <button className="square">X</button>;

}

在board.css中添加

css 复制代码
.square {
    background: #fff;
    border: 1px solid #999;
    float: left;
    font-size: 24px;
    font-weight: bold;
    line-height: 34px;
    height: 34px;
    margin-right: -1px;
    margin-top: -1px;
    padding: 0;
    text-align: center;
    width: 34px;
}

在 React 中,组件是一段可重用代码,它通常作为 UI 界面的一部分。组件用于渲染、管理和更新应用中的 UI 元素。让我们逐行查看这段代码,看看发生了什么:

1、第一行定义了一个名为 Board 的函数。JavaScript 的 export 关键字使此函数可以在此文件之外访问。default 关键字表明它是文件中的主要函数。

2、第二行返回一个按钮。JavaScript 的 return 关键字意味着后面的内容都作为值返回给函数的调用者。<button> 是一个 JSX 元素。JSX 元素是 JavaScript 代码和 HTML 标签的组合,用于描述要显示的内容。className="square" 是一个 button 属性,它决定 CSS 如何设置按钮的样式。X 是按钮内显示的文本,</button> 闭合 JSX 元素以表示不应将任何后续内容放置在按钮内。

为了让该组件可以展示在网页中,我们需要将Board组件导出,并在App组件中导入

javascript 复制代码
import Board from "./components/Board/Board.jsx";

export default function App() {
    return <Board/>;
}

浏览器会像下面这样在方块里面显示一个 X:

目前棋盘只有一个方块,但你需要九个!如果你只是想着复制粘贴来制作两个像这样的方块:

javascript 复制代码
export default function Board() {

  return <button className="square">X</button><button className="square">X</button>;

}

你将会得到如下错误:

D:\Desktop\tictactoe\src\components\Board\Board.jsx: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

React 组件必须返回单个 JSX 元素,不能像两个按钮那样返回多个相邻的 JSX 元素。要解决此问题,可以使用 Fragment(<></>)包裹多个相邻的 JSX 元素,如下所示:

javascript 复制代码
export default function Board() {

  return (

    <>

      <button className="square">X</button>

      <button className="square">X</button>

    </>

  );

}

现在你应该可以看见:

非常棒!现在你只需要通过复制粘贴来添加九个方块,然后......

但事与愿违的是这些方块并没有排列成网格,而是都在一条线上。要解决此问题,需要使用 div 将方块分到每一行中并添加一些 CSS 样式。当你这样做的时候,需要给每个方块一个数字,以确保你知道每个方块的位置。

css 复制代码
.board-row:after {
  clear: both;
  content: '';
  display: table;
}

App.js 文件中,Square 组件看起来像这样:

javascript 复制代码
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>

    </>

  );

}

借助 board.css 中定义的 board-row 样式,我们将组件分到每一行的 div 中。最终完成了井字棋棋盘:

通过 props 传递数据

接下来,当用户单击方块时,我们要将方块的值从空更改为"X"。根据目前构建的棋盘,你需要复制并粘贴九次更新方块的代码(每个方块都需要一次)!但是,React 的组件架构可以创建可重用的组件,以避免混乱、重复的代码。

首先,要将定义第一个方块(<button className="square">1</button>)的这行代码从 Board 组件复制到新的 Square 组件中:

javascript 复制代码
function Square() {
  return <button className="square">1</button>;
}

export default function Board() {
  // ...
}

然后,更新 Board 组件并使用 JSX 语法渲染 Square 组件:

javascript 复制代码
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>
        </>
    );
}

需要注意的是,这并不像 div,这些你自己的组件如 BoardSquare,必须以大写字母开头。

让我们来看一看效果:

哦不!你失去了你以前有正确编号的方块。现在每个方块都写着"1"。要解决此问题,需要使用 props 将每个方块应有的值从父组件(Board)传递到其子组件(Square)。

更新 Square 组件,读取从 Board 传递的 value props:

javascript 复制代码
function Square({ value }) {

  return <button className="square">1</button>;

}

function Square({ value }) 表示可以向 Square 组件传递一个名为 value 的 props。

现在你如果想要显示对应的 value 而不是 1,可以试一下像下面这样:

javascript 复制代码
function Square({ value }) {

  return <button className="square">value</button>;

}

糟糕!这还不是你想要的:

我们需要从组件中渲染名为 value 的 JavaScript 变量,而不是"value"这个词。要从 JSX"转义到 JavaScript",你需要使用大括号。在 JSX 中的 value 周围添加大括号,如下所示:

javascript 复制代码
function Square({ value }) {

  return <button className="square">{value}</button>;

}

现在,你应该会看到一个空的棋盘了:

这是因为 Board 组件还没有将 value props 传递给它渲染的每个 Square 组件。要修复这个问题,需要向 Board 组件里面的每个 Square 组件添加 value props:

ini 复制代码
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>
        </>
    );
}

现在你应该能再次看到数字网格:

创建一个具有交互性的组件

当你单击它的时候,Square 组件需要显示"X"。在 Square 内部声明一个名为 handleClick 的函数。然后,将 onClick 添加到由 Square 返回的 JSX 元素的 button 的 props 中:

javascript 复制代码
function Square({ value }) {
    function handleClick() {
        console.log('clicked!');
    }

    return (
        <button
            className="square"
            onClick={handleClick}
        >
            {value}
        </button>
    );
}

打开开发者工具的控制台,如果现在单击一个方块,你应该会看到一条日志,上面写着 "clicked!"。多次单击方块将再次记录 "clicked!"。具有相同消息的重复控制台日志不会在控制台中重复创建。而你会在第一次 "clicked!" 旁边看到一个递增的计数器。

下一步,我们希望 Square 组件能够"记住"它被单击过,并用"X"填充它。为了"记住"一些东西,组件使用 state

React 提供了一个名为 useState 的特殊函数,可以从组件中调用它来让它"记住"一些东西。让我们将 Square 的当前值存储在 state 中,并在单击 Square 时更改它。

在文件的顶部导入 useState。从 Square 组件中移除 value props。在调用 useStateSquare 的开头添加一个新行。让它返回一个名为 value 的 state 变量:

javascript 复制代码
import { useState } from 'react';

function Square() {

  const [value, setValue] = useState(null);

  function handleClick() {

    //...

value 存储值,而 setValue 是可用于更改值的函数。传递给 useStatenull 用作这个 state 变量的初始值,因此此处 value 的值开始时等于 null

由于 Square 组件不再接受 props,我们从 Board 组件创建的所有九个 Square 组件中删除 value props:

javascript 复制代码
// ...

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>
    </>

  );

}

现在将更改 Square 以在单击时显示"X"。不再使用 console.log("clicked!"); 而使用 setValue('X'); 的事件处理程序。现在你的 Square 组件看起来像这样:

javascript 复制代码
function Square() {
  const [value, setValue] = useState(null);

  function handleClick() {
    setValue('X');
  }

  return (
    <button
      className="square"
      onClick={handleClick}
    >
      {value}
    </button>
  );

}

通过从 onClick 处理程序调用此 set 函数,你告诉 React 在单击其 <button> 时要重新渲染该 Square。更新后,方块的值将为"X",因此会在棋盘上看到"X"。点击任意方块,"X"应该出现

每个 Square 都有自己的 state:存储在每个 Square 中的 value 完全独立于其他的 Square。当你在组件中调用 set 函数时,React 也会自动更新内部的子组件。

完成这个游戏

至此,你已经拥有井字棋游戏的所有基本构建块。要玩完整的游戏,你现在需要在棋盘上交替放置"X"和"O",并且你需要一种确定获胜者的方法。

状态提升

目前,每个 Square 组件都维护着游戏 state 的一部分。要检查井字棋游戏中的赢家,Board 需要以某种方式知道 9 个 Square 组件中每个组件的 state。

你会如何处理?起初,你可能会猜测 Board 需要向每个 Square"询问"Square 的 state。尽管这种方法在 React 中在技术上是可行的,但我们不鼓励这样做,因为代码变得难以理解、容易出现错误并且难以重构。相反,最好的方法是将游戏的 state 存储在 Board 父组件中,而不是每个 Square 中。Board 组件可以通过传递一个 props 来告诉每个 Square 显示什么,就像你将数字传递给每个 Square 时所做的那样。

要从多个子组件收集数据,或让两个子组件相互通信,请改为在其父组件中声明共享 state。父组件可以通过 props 将该 state 传回给子组件。这使子组件彼此同步并与其父组件保持同步。

重构 React 组件时,将状态提升到父组件中很常见。

让我们借此机会尝试一下。编辑 Board 组件,使其声明一个名为 squares 的 state 变量,该变量默认为对应于 9 个方块的 9 个空值数组:

javascript 复制代码
// ...

export default function Board() {

  const [squares, setSquares] = useState(Array(9).fill(null));

  return (

    // ...

  );

}

Array(9).fill(null) 创建了一个包含九个元素的数组,并将它们中的每一个都设置为 null。包裹它的 useState() 声明了一个初始设置为该数组的 squares state 变量。数组中的每个元素对应于一个 square 的值。当你稍后填写棋盘时,squares 数组将如下所示:

csharp 复制代码
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]

现在你的 Board 组件需要将 value props 向下传递给它渲染的每个 Square

javascript 复制代码
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>
        </>
    );
}

接下来,你将编辑 Square 组件,以从 Board 组件接收 value props。这将需要删除 Square 组件自己的 value state 和按钮的 onClick props:

javascript 复制代码
function Square({value}) {

  return <button className="square">{value}</button>;

}

此时你应该看到一个空的井字棋棋盘:

现在,每个 Square 都会收到一个 value props,对于空方块,该 props 将是 'X''O'null

接下来,你需要更改单击 Square 时发生的情况。Board 组件现在维护已经填充过的方块。你需要为 Square 创建一种更新 Board state 的方法。由于 state 对于定义它的组件是私有的,因此你不能直接从 Square 更新 Board 的 state。

你将从 Board 组件向下传递一个函数到 Square 组件,然后让 Square 在单击方块时调用该函数。我们将从单击 Square 组件时将调用的函数开始。调用该函数 onSquareClick

javascript 复制代码
function Square({ value }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

接下来,将 onSquareClick 函数添加到 Square 组件的 props 中:

javascript 复制代码
function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}

现在,你将把 onSquareClick props 连接到 Board 组件中的一个函数,命名为 handleClick。要将 onSquareClick 连接到 handleClick,需要将一个函数传递给第一个 Square 组件的 onSquareClick props:

ini 复制代码
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={handleClick} />
        //...
  );
}

最后,你将在 Board 组件内定义 handleClick 函数来更新并保存棋盘 state 的 squares 数组:

scss 复制代码
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  
  function handleClick() {
    const nextSquares = squares.slice();
    nextSquares[0] = "X";
    setSquares(nextSquares);
  }
  
  return (
    // ...
  )
}

handleClick 函数使用 JavaScript 数组的 slice() 方法创建 squares 数组(nextSquares)的副本。然后,handleClick 更新 nextSquares 数组,将 X 添加到第一个([0] 索引)方块。

调用 setSquares 函数让 React 知道组件的 state 已经改变。这将触发使用 squares state 的组件(Board)及其子组件(构成棋盘的 Square 组件)的重新渲染。

注意

JavaScript 支持闭包,这意味着内部函数(例如 handleClick)可以访问外部函数(例如 Board)中定义的变量和函数。handleClick 函数可以读取 squares state 并调用 setSquares 方法,因为它们都是在 Board 函数内部定义的。

现在你可以将 X 添加到棋盘上......但只能添加到左上角的方块。你的 handleClick 函数被硬编码为更新左上角方块( 0)的索引。让我们更新 handleClick 以便能够更新任何方块。将参数 i 添加到 handleClick 函数,该函数采用要更新的 square 索引:

scss 复制代码
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 (
    // ...
  )
}

接下来,你需要将 i 传递给 handleClick。你可以尝试像这样在 JSX 中直接将 square 的 onSquareClick props 设置为 handleClick(0),但这是行不通的:

scss 复制代码
<Square value={squares[0]} onSquareClick={handleClick(0)} />

为什么会是这样呢?handleClick(0) 调用将成为渲染 Board 组件的一部分。因为 handleClick(0) 通过调用 setSquares 改变了棋盘组件的 state,所以你的整个棋盘组件将再次重新渲染。但这再次运行了 handleClick(0),导致无限循环:

为什么这个问题没有早点发生?

当你传递 onSquareClick={handleClick} 时,你将 handleClick 函数作为 props 向下传递。你不是在调用它!但是现在你立即调用了该函数------注意 handleClick(0) 中的括号------这就是它运行得太早的原因。你不想在用户点击之前调用 handleClick

你可以通过创建调用 handleClick(0) 的函数(如 handleFirstSquareClick)、调用 handleClick(1) 的函数(如 handleSecondSquareClick)等来修复。你可以将这些函数作为 onSquareClick={handleFirstSquareClick} 之类的 props 传递(而不是调用)。这将解决无限循环的问题。

但是,定义九个不同的函数并为每个函数命名过于冗余。让我们这样做:

scss 复制代码
export default function Board() {

  // ...

  return (
    <>
      <div className="board-row">
        <Square value={squares[0]} onSquareClick={() => handleClick(0)} />
        // ...
  );
}

注意新的 () => 语法。这里,() => handleClick(0) 是一个箭头函数,它是定义函数的一种较短的方式。单击方块时,=>"箭头"之后的代码将运行,调用 handleClick(0)

现在你需要更新其他八个方块以从你传递的箭头函数中调用 handleClick。确保 handleClick 的每次调用的参数对应于正确的 square 索引:

scss 复制代码
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>
    </>

  );

};

现在你可以再次通过单击将 X 添加到棋盘的方块上: 但是这次所有的 state 管理都由 Board 组件处理!

现在,我们在 Board 组件中处理 state, Board 父组件将 props 传递给 Square 子组件,以便它们可以正确显示。单击 Square 时, Square 子组件现在要求 Board 父组件更新棋盘的 state。当 Board 的 state 改变时,Board 组件和每个子 Square 都会自动重新渲染。保存 Board 组件中所有方块的 state 将使得它可以确定未来的赢家。

让我们回顾一下当用户单击你的棋盘左上角的方块以向其添加 X 时会发生什么:

  1. 单击左上角的方块运行 buttonSquare 接收到的 onClick props 的函数。Square 组件从 Board 通过 onSquareClick props 接收到该函数。Board 组件直接在 JSX 中定义了该函数。它使用参数 0 调用 handleClick
  2. handleClick 使用参数(0)将 squares 数组的第一个元素从 null 更新为 X
  3. Board 组件的 squares state 已更新,因此 Board 及其所有子组件都将重新渲染。这会导致索引为 0Square 组件的 value props 从 null 更改为 X

最后,用户看到左上角的方块在单击后从空变为 X

注意

DOM <button> 元素的 onClick props 对 React 有特殊意义,因为它是一个内置组件。对于像 Square 这样的自定义组件,命名由你决定。你可以给 SquareonSquareClick props 或 BoardhandleClick 函数起任何名字,代码还是可以运行的。在 React 中,通常使用 onSomething 命名代表事件的 props,使用 handleSomething 命名处理这些事件的函数。

为什么不变性很重要

请注意在 handleClick 中,你调用了 .slice() 来创建 squares 数组的副本而不是修改现有数组。为了解释原因,我们需要讨论不变性以及为什么学习不变性很重要。

通常有两种更改数据的方法。第一种方法是通过直接更改数据的值来改变数据。第二种方法是使用具有所需变化的新副本替换数据。如果你改变 squares 数组,它会是这样的:

csharp 复制代码
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];

如果你在不改变 squares 数组的情况下更改数据,它会是这样的:

csharp 复制代码
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`

结果是一样的,但通过不直接改变(改变底层数据),你可以获得几个好处。

不变性使复杂的功能更容易实现。在本教程的后面,你将实现一个"时间旅行"功能,让你回顾游戏的历史并"跳回"到过去的动作。此功能并非特定于游戏------撤消和重做某些操作的能力是应用程序的常见要求。避免数据直接突变可以让你保持以前版本的数据完好无损,并在以后重用它们。

不变性还有另一个好处。默认情况下,当父组件的 state 发生变化时,所有子组件都会自动重新渲染。这甚至包括未受变化影响的子组件。尽管重新渲染本身不会引起用户注意(你不应该主动尝试避免它!),但出于性能原因,你可能希望跳过重新渲染显然不受其影响的树的一部分。不变性使得组件比较其数据是否已更改的成本非常低。你可以在 memo API 参考 中了解更多关于 React 如何选择何时重新渲染组件的信息。

交替落子

现在是时候修复这个井字棋游戏的一个主要缺陷了:棋盘上无法标记"O"。

默认情况下,你会将第一步设置为"X"。让我们通过向 Board 组件添加另一个 state 来跟踪这一点:

scss 复制代码
function Board() {
  const [xIsNext, setXIsNext] = useState(true);

  const [squares, setSquares] = useState(Array(9).fill(null));

  // ...

}

每次玩家落子时,xIsNext(一个布尔值)将被翻转以确定下一个玩家,游戏 state 将被保存。你将更新 BoardhandleClick 函数以翻转 xIsNext 的值:

scss 复制代码
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 (
    //...
  );

}

现在,当你点击不同的方块时,它们会在 XO 之间交替,这是它们应该做的!

但是等等,有一个问题。尝试多次点击同一个方块:

XO 覆盖!虽然这会给游戏带来非常有趣的变化,但我们现在将坚持原来的规则。

当你用 XO 标记方块时,你没有检查该方块是否已经具有 XO 值。你可以通过提早返回来解决此问题。我们将检查方块是否已经有 XO。如果方块已经填满,你将尽早在 handleClick 函数中 return------在它尝试更新棋盘 state 之前。

ini 复制代码
function handleClick(i) {

  if (squares[i]) {

    return;

  }

  const nextSquares = squares.slice();

  //...

}

现在你只能将 XO 添加到空方块中!

宣布获胜者

现在你可以轮流对战了,接下来我们应该显示游戏何时获胜。为此,你将添加一个名为 calculateWinner 的辅助函数,它接受 9 个方块的数组,检查获胜者并根据需要返回 'X''O'null。不要太担心 calculateWinner 函数;它不是 React 才会有的:

csharp 复制代码
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;

}

你将在 Board 组件的 handleClick 函数中调用 calculateWinner(squares) 来检查玩家是否获胜。你可以在检查用户是否单击了已经具有 XO 的方块的同时执行此检查。在这两种情况下,我们都希望尽早返回:

ini 复制代码
function handleClick(i) {

  if (squares[i] || calculateWinner(squares)) {

    return;

  }

  const nextSquares = squares.slice();

  //...

}

为了让玩家知道游戏何时结束,你可以显示"获胜者:X"或"获胜者:O"等文字。为此,你需要将 status 部分添加到 Board 组件。如果游戏结束,将显示获胜者,如果游戏正在进行,你将显示下一轮将会是哪个玩家:

ini 复制代码
export default function Board() {

  // ...

  const winner = calculateWinner(squares);

  let status;

  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (xIsNext ? "X" : "O");
  }


  return (

    <>
      <div className="status">{status}</div>
      <div className="board-row">
        // ...

  )

}
css 复制代码
.status {
  margin-bottom: 10px;
}

恭喜!你现在有一个可以运行的井字棋游戏。你也学习了 React 的基础知识。所以你是这里真正的赢家。代码应该如下所示:

ini 复制代码
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 = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (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;
}

添加时间旅行

其实就是使用state保存状态,同学们自己动手尝试一下

如果做不出来就参照教程:井字棋游戏 -- React 中文文档

结束

相信通过这个教程你对React的使用有了一定的了解,但是学习React你还需要学习更多

可以从看官方文档开始,React 官方中文文档,逐步学习每个概念

加油加油加油!!!

相关推荐
南囝coding12 小时前
React 19.2 重磅更新!这几个新特性终于来了
前端·react.js·preact
qq. 280403398417 小时前
react hooks
前端·javascript·react.js
PairsNightRain18 小时前
React Concurrent Mode 是什么?怎么使用?
前端·react.js·前端框架
小岛前端19 小时前
React 剧变!
前端·react.js·前端框架
用户47949283569151 天前
面试官:讲讲这段react代码的输出(踩坑)
前端·javascript·react.js
GISer_Jing1 天前
React中Element、Fiber、createElement和Component关系
前端·react.js·前端框架
lvchaoq1 天前
react 修复403页面无法在首页跳转问题
前端·javascript·react.js
郝开1 天前
6. React useState基础使用:useState修改状态的规则;useState修改对象状态的规则
前端·javascript·react.js
Codigger官方1 天前
Linux 基金会牵头成立 React 基金会:前端开源生态迎来里程碑式变革
linux·前端·react.js
ObjectX前端实验室1 天前
【图形编辑器架构】🧠 Figma 风格智能选择工具实现原理【猜测】
前端·react.js