快速学完React计划(第0天)-快速入门

前言:学习自React官方中文文档,大部分为摘抄,其中有改动,加入自己的理解,供自己入门使用。已有typescipt、css、html、vue基础。看看自己多久能学完并且做项目!(只是为了监督懒惰的自己www)

目录

快速入门

创建和嵌套组件

React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。

React 组件是返回标签的 JavaScript 函数

function MyButton() {
  return (
    <button>我是一个按钮</button>
  );
}

至此,你已经声明了 MyButton,现在把它嵌套到另一个组件中:

export default function MyApp() {
  return (
    <div>
    <h1>欢迎来到我的应用</h1>
    <MyButton />
    </div>
  );
}

React 组件必须以大写字母开头,而 HTML 标签则必须是小写字母。

使用 JSX 编写标签

上面所使用的标签语法被称为 JSX

JSX 比 HTML 更加严格。你必须闭合标签,如 <br />。你的组件也不能返回多个 JSX 标签。你必须将它们包裹到一个共享的父级中,比如 <div>...</div> 或使用空的 <>...</> 包裹:

function AboutPage() {
  return (
    <>
      <h1>关于</h1>
      <p>你好。<br />最近怎么样?</p>
    </>
  );
}

添加样式

在 React 中,你可以使用 className 来指定一个 CSS 的 class。它与 HTML 的 class 属性的工作方式相同:

<img className="avatar" />

然后,你可以在一个单独的 CSS 文件中为它编写 CSS 规则:


.avatar {
  border-radius: 50%;

}

React 并没有规定你如何添加 CSS 文件。最简单的方式是使用 HTML 的 <link> 标签。

展示变量

1. 基础:在标签内容中展示变量

当需要把 JavaScript 变量的值显示在标签里时,用{}包裹变量,JSX 会自动解析并展示结果。

// 定义数据
const user = {
  name: "李华",
  age: 25
};

// 组件中展示
function UserInfo() {
  return (
    <div>
      <p>姓名:{user.name}</p>  {/* 显示 user.name 的值:"李华" */}
      <p>年龄:{user.age}</p>   {/* 显示 user.age 的值:25 */}
    </div>
  );
}

2. 在属性中使用变量(替代字符串)

JSX 标签的属性默认用""包裹字符串,但如果属性值来自 JavaScript 变量,必须用{}包裹变量。

const user = {
  avatarUrl: "https://example.com/avatar.jpg", // 图片地址变量
  nickname: "小花"
};

function UserAvatar() {
  return (
    // src属性值来自 user.avatarUrl 变量,用{}包裹
    <img 
      src={user.avatarUrl}  {/* 等价于 src="https://example.com/avatar.jpg" */}
      alt={"用户" + user.nickname + "的头像"}  {/* 字符串拼接后作为alt属性 */}
    />
  );
}

注意:

  • src="user.avatarUrl" 会被当作字符串 "user.avatarUrl",而不是变量
  • src={user.avatarUrl} 才会读取变量的值

3. 嵌入复杂表达式

{}中不仅能放变量,还能放任何有返回值的 JavaScript 表达式(计算、拼接、三元运算等)。

const product = {
  name: "笔记本电脑",
  price: 4999,
  stock: 3
};

function ProductInfo() {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>原价:{product.price}元</p>
      <p>折扣价:{product.price * 0.9}元</p>  {/* 计算:9折后的价格 */}
      <p>库存状态:{product.stock > 0 ? "有货" : "售罄"}</p>  {/* 三元运算判断状态 */}
    </div>
  );
}

4. 结合样式(style 属性)

当样式依赖 JavaScript 变量时,用style={{}}语法:外层{}表示嵌入 JavaScript,内层{}是样式对象(键为驼峰式 CSS 属性)。

const config = {
  fontSize: 16, // 字体大小变量
  color: "blue"
};

function StyledText() {
  return (
    <p 
      style={{ 
        fontSize: config.fontSize + "px",  // 用变量设置字体大小(拼接单位)
        color: config.color,               // 用变量设置颜色
        fontWeight: "bold"                 // 直接写固定样式值
      }}
    >
      这是一段带样式的文本
    </p>
  );
}

条件渲染

在 React 中,条件渲染完全依赖 普通 JavaScript 语法 实现,没有专门的特殊语法。以下是最常用的几种方式,按使用场景整理:

1. if...else 语句(适合复杂逻辑 / 多分支)

当条件判断较复杂,或需要处理多个分支时,适合先用 if...else 定义不同的 JSX 内容,再在渲染中引用。

function UserPage({ isLoggedIn }) {
  // 用变量存储不同条件下的 JSX
  let pageContent;
  if (isLoggedIn) {
    // 已登录:显示用户面板
    pageContent = <UserDashboard />;
  } else {
    // 未登录:显示登录表单
    pageContent = <LoginForm />;
  }

  // 渲染存储的 JSX
  return <div>{pageContent}</div>;
}

优点:逻辑清晰,适合多条件(如 if...else if...else)或需要额外处理的场景。

2. 三元运算符(适合 JSX 内部的简单分支)

如果条件较简单,且希望直接在 JSX 中写判断,可以用 条件 ? 结果1 : 结果2 的三元语法。

function Greeting({ isLoggedIn }) {
  return (
    <div>
      {/* 直接在 JSX 中通过三元判断显示内容 */}
      {isLoggedIn ? (
        <p>欢迎回来,用户!</p>  // 条件为 true 时显示
      ) : (
        <p>请先登录~</p>       // 条件为 false 时显示
      )}
    </div>
  );
}

适用场景:只有两个分支,且逻辑简单,希望代码更紧凑。

3. 逻辑 && 运算符(适合 “仅当条件为真时显示”)

如果只需要在条件为 true 时显示内容,没有 else 分支,可用 条件 && 内容 简化代码。

function Notification({ hasUnread }) {
  return (
    <div>
      <h1>消息中心</h1>
      {/* 只有 hasUnread 为 true 时,才显示未读提示 */}
      {hasUnread && <p>您有未读消息!</p>}
    </div>
  );
}

👉 原理:JavaScript 中 true && 表达式 会返回表达式,false && 表达式 会返回 false,而 React 会忽略 false 的渲染。

4. 条件性设置属性

以上逻辑也可用于动态设置标签的属性(如是否禁用按钮、是否添加类名等)。

function SubmitButton({ isReady }) {
  return (
    <button
      // 条件为 true 时,添加 disabled 属性(值为 true);否则不添加
      disabled={!isReady}
      // 条件为 true 时,类名为 "active";否则为 "inactive"
      className={isReady ? "active" : "inactive"}
    >
      提交
    </button>
  );
}

渲染列表

在 React 中,渲染列表完全依赖 JavaScript 原生的数组方法(如 map())或循环语句,核心是将数组数据转换为 JSX 元素列表。以下是具体梳理:

一、基本用法:用 map() 渲染列表

map() 是数组的原生方法,能遍历数组并返回新的元素集合,非常适合在 React 中将数据数组转换为 JSX 列表。

  1. 定义数据源:先有一个包含多条数据的数组(通常来自状态或 props)。
const fruits = [
  { name: '草莓', id: 1 },
  { name: '香蕉', id: 2 },
  { name: '芒果', id: 3 }
];
  1. map() 转换为 JSX 列表:遍历数组,为每个元素返回对应的 JSX 标签(如 <li>)。
// 遍历 fruits 数组,生成 <li> 元素集合
const fruitItems = fruits.map(fruit => {
  return (
    <li key={fruit.id}>  {/* 注意:每个元素必须有 key 属性 */}
      {fruit.name}
    </li>
  );
});
  1. 在组件中渲染列表:将转换后的 JSX 列表嵌入到父标签(如 <ul><ol>)中。
function FruitList() {
  return <ul>{fruitItems}</ul>;  // 渲染 <li> 列表
}

最终渲染结果相当于:

<ul>
  <li>草莓</li>
  <li>香蕉</li>
  <li>芒果</li>
</ul>

二、关键:key 属性的作用与规则

每个列表项必须添加 key 属性,这是 React 识别列表元素的“身份标识”,直接影响渲染性能。

作用:

  • 当列表数据发生变化(如新增、删除、排序)时,React 通过 key 快速识别哪些元素没变、哪些变了,从而只更新变化的部分(而非重新渲染整个列表),提升效率。

规则:

  1. 唯一性key 在兄弟节点中必须唯一(不需要全局唯一)。
    例:fruit.id 是数据库中唯一的 ID,适合作为 key
  2. 稳定性key 的值应固定不变(除非元素本身被删除)。
    ❌ 不建议用数组索引(index)作为 key(如果列表会重新排序/删除元素,索引会变化,导致 React 误判元素身份)。
    ✅ 优先用数据中自带的唯一标识(如 iduuid 等)。

三、简化写法:直接在 JSX 中嵌入 map()

实际开发中,常将 map() 直接写在渲染的 JSX 中,减少中间变量:

function FruitList() {
  const fruits = [
    { name: '草莓', id: 1 },
    { name: '香蕉', id: 2 },
    { name: '芒果', id: 3 }
  ];

  return (
    <ul>
      {/* 直接在 JSX 中用 map() 生成列表项 */}
      {fruits.map(fruit => (
        <li key={fruit.id}>
          {fruit.name}
        </li>
      ))}
    </ul>
  );
}

井字棋游戏教程

概览

App.js

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

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

index.js

它是 App.js 文件中创建的组件与 Web 浏览器之间的桥梁

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';

import App from './App';

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

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

构建棋盘

让我们回到 App.js。接下来我们将专注于这个文件。

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

export default function Square() {
  return <button className="square">X</button><button className="square">X</button>;

}

错误的!

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

export default function Square() {
  return (
    <>
    <button className="square">X</button>
    <button className="square">X</button>
    </>
  );
}

要把9个方块变成正方形,要添加CSS样式

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

通过props传递数据

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

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

function Square({ value }) {
  return <button className="square">{value}</button>;
}

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

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”填充它。为了“记住”一些东西,组件使用 state

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

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

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

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

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

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

React开发者工具

React 开发者工具可以检查 React 组件的 props 和 state。可以在 CodeSandbox 的 Browser 部分底部找到 React DevTools 选项卡:

状态提升

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

最好的方法是将游戏的 state 存储在 Board 父组件中,而不是每个 Square 中。Board 组件可以通过传递一个 props 来告诉每个 Square 显示什么,就像你将数字传递给每个 Square 时所做的那样。

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

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

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

// ...
export default function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  return (
    // ...
  );
}

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

['O', null, 'X', 'X', 'X', 'O', 'O', null, null]

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

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:

function Square({value}) {
  return <button className="square">{value}</button>;
}

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

  1. 状态私有性:组件的 state 是其内部封装的数据,仅由组件自身管理。根据 React 设计原则,子组件不能直接访问或修改父组件的 state,这是为了保证组件的独立性和数据流向的可预测性。
  2. 单向数据流:数据只能从父组件通过 props 传递给子组件(向下流动);子组件若需影响父组件的数据,必须通过父组件预先提供的 “接口”(通常是回调函数)反向通知,而非直接修改

Board 组件向下传递一个函数到 Square 组件,然后让 Square 在单击方块时调用该函数。

1. 子组件(Square)声明事件接收接口

function Square({ value, onSquareClick }) {
  return (
    <button className="square" onClick={onSquareClick}>
      {value}
    </button>
  );
}
  • Square 组件通过 props 接收 onSquareClick,这是一个从父组件(Board)传递过来的回调函数。
  • buttononClick 事件绑定 onSquareClick,意味着:当用户点击该按钮时,会执行 onSquareClick 指向的函数。

2. 父组件(Board)定义状态更新逻辑

export default function Board() {
  // 声明状态:squares 数组记录棋盘每个位置的状态('X'/'O'/null)
  const [squares, setSquares] = useState(Array(9).fill(null));

  // 定义状态更新函数:接收索引 i,更新对应位置为 'X'
  function handleClick(i) {
    // 关键:通过 slice() 创建 squares 的副本(保持数据不可变性)
    const nextSquares = squares.slice(); 
    nextSquares[i] = 'X'; // 修改副本中索引 i 的值
    setSquares(nextSquares); // 调用 setState 更新状态,触发重渲染
  }

  // ...
}
  • handleClickBoard 内部的函数,负责更新 squares 状态。它利用 React 的 setSquares 方法(由 useState 提供)触发状态更新,进而导致组件重渲染。
  • 这里使用 slice() 创建副本而非直接修改 squares(如 squares[i] = 'X'),是为了遵循 React 对 “不可变数据” 的推荐 —— 避免直接突变状态,确保状态更新的可追踪性(便于后续时间旅行等功能)。

3. 父组件向子组件传递回调函数(带参数)

// 在 Board 的 return 中,向每个 Square 传递 onSquareClick
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
// ... 其他 Square
  • 核心:传递给 onSquareClick 的是一个箭头函数() => handleClick(i)),而非直接调用 handleClick(i)
    • 若直接写 onSquareClick={handleClick(0)},则在 Board 渲染时会立即执行 handleClick(0),导致 setSquares 被调用,触发重渲染;重渲染时又会再次执行 handleClick(0),形成无限循环(控制台报错 Too many re-renders 的原因)。
    • 箭头函数 () => handleClick(i) 是一个 “待执行” 的函数引用:只有当 Square 中的按钮被点击时,才会执行 handleClick(i),避免了渲染阶段的提前调用。

为什么重新渲染会再吃执行handleClick函数?

当一个 React 组件(比如 Board)需要渲染(或重新渲染)时,会执行以下步骤:

  • 执行组件函数体(function Board() { ... } 内的所有代码)。
  • 生成 JSX 结构(即 return 中的内容)。
  • 将 JSX 转换为 DOM 操作,更新页面。

让我们回顾一下当用户单击你的棋盘左上角的方块以向其添加 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

在 React 中,通常使用 onSomething 命名代表事件的 props,使用 handleSomething 命名处理这些事件的函数。

交替落子

每次玩家落子时,xIsNext(一个布尔值)将被翻转以确定下一个玩家,游戏 state 将被保存。你将更新 BoardhandleClick 函数以翻转 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 (
    //...
  );
}

为了防止一个方格可以被重新覆盖:

function handleClick(i) {
  if (squares[i]) {
    return;
  }
  const nextSquares = squares.slice();
  //...
}

宣布获胜者

很巧妙的判断方法

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 的方块的同时执行此检查。在这两种情况下,我们都希望尽早返回:

function handleClick(i) {
  if (squares[i] || calculateWinner(squares)) {
    return;
  }
  const nextSquares = squares.slice();
  //...
}

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

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">
        // ...
  )
}

存储落子历史

如果你改变了 squares 数组,实现时间旅行将非常困难。

但是,你在每次落子后都使用 slice() 创建 squares 数组的新副本,并将其视为不可变的。这将允许你存储 squares 数组的每个过去的版本,并在已经发生的轮次之间“来回”。

把过去的 squares 数组存储在另一个名为 history 的数组中,把它存储为一个新的 state 变量。history 数组表示所有棋盘的 state,从第一步到最后一步,其形状如下:

[
  // 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'],
  // ...

]

再一次“状态提升”

你现在将编写一个名为 Game 的新顶级组件来显示过去的着法列表。这就是放置包含整个游戏历史的 history state 的地方。

history state 放入 Game ,这使 Game 组件可以完全控制 Board 的数据,并使它让 Board 渲染来自 history 的之前的回合。

首先,添加一个带有 export defaultGame 组件。让它渲染 Board 组件和一些标签。

要删除 function Board() { 声明之前的 export default 关键字,并将它们添加到 function Game() { 声明之前。这会告诉你的 index.js 文件使用 Game 组件而不是你的 Board 组件作为顶层组件。

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

Game 组件添加一些 state 以跟踪下一个玩家和落子历史:

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  // ...

要渲染当前落子的方块,你需要从 history 中读取最后一个 squares 数组。你不需要 useState——你已经有足够的信息可以在渲染过程中计算它:

export default function Game() {
  const [xIsNext, setXIsNext] = useState(true);
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

接下来,在 Game 组件中创建一个 handlePlay 函数,Board 组件将调用该函数来更新游戏。将 xIsNextcurrentSquareshandlePlay 作为 props 传递给 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} />
    //...
    )
}

现在,删除调用 useStateBoard 函数的前两行将 Board 组件里面的 handleClick 中的 setSquaressetXIsNext 调用替换为对新 onPlay 函数的一次调用,这样 Game 组件就可以在用户单击方块时更新 Board

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

Board 组件完全由 Game 组件传递给它的 props 控制。你需要在 Game 组件中实现 handlePlay 函数才能使游戏重新运行。

handlePlay 被调用应该做什么?请记住,Board 以前使用更新后的数组调用 setSquares;现在它将更新后的 squares 数组传递给 onPlay

handlePlay 函数需要更新 Game 的 state 以触发重新渲染,但是你没有可以再调用的 setSquares 函数——你现在正在使用 history state 变量来存储这些信息。你需要追加更新后的 squares 数组来更新 history 作为新的历史入口。你还需要切换 xIsNext,就像 Board 过去所做的那样:

export default function Game() {
  //...
  function handlePlay(nextSquares) {
    setHistory([...history, nextSquares]);
    setXIsNext(!xIsNext);
  }
  //...
}

在这里,[...history, nextSquares] 创建了一个新数组,其中包含 history 中的所有元素,后跟 nextSquares。(你可以将 ...history 展开语法理解为“枚举 history 中的所有元素”。)

例如,如果 history[[null,null,null], ["X",null,null]]nextSquares["X",null,"O"],那么新的 [...history, nextSquares] 数组就是 [[null,null,null], ["X",null,null], ["X",null,"O"]]

此时,你已将 state 移至 Game 组件中,并且 UI 应该完全正常工作,就像重构之前一样。这是此时代码的样子:

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

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

选择Key

你需要为每个列表项指定一个 key 属性,以将每个列表项与其兄弟项区分开来。如果你的数据来自数据库,数据库 ID 可以用作 key:

<li key={user.id}>
  {user.name}: {user.taskCount} tasks left
</li>
  1. key 的作用:作为列表项的“身份标识”,帮助 React 在重新渲染时识别每个列表项:
    • 新 key 出现 → 创建新组件;
    • 旧 key 消失 → 销毁对应组件;
    • key 匹配 → 复用原有组件(保持状态)。
  1. key 与状态的关系:key 决定组件是否复用:
    • key 不变 → 组件复用,状态保留;
    • key 变化 → 组件销毁重建,状态重置。
  1. key 的特殊性
    • 是 React 内部使用的保留属性,不算作子组件的 props,子组件无法获取;
    • 只需在同级列表项中唯一,无需全局唯一。
  1. 使用规范
    • 动态列表必须指定合适的 key(优先用数据自带的唯一标识,如 user.id);
    • 禁止用数组索引作 key(会在增删排序时出问题),即使显式传 key={i} 也不推荐;
    • 若没有合适的 key,应调整数据结构以提供唯一标识。

实现时间旅行

在井字棋游戏的历史中,过去的每一步都有一个唯一的 ID 与之相关联:它是动作的序号。落子永远不会被重新排序、删除或从中间插入,因此使用落子的索引作为 key 是安全的。

Game 函数中,你可以将 key 添加为 <li key={move}>,如果你重新加载渲染的游戏,React 的“key”错误应该会消失:

import { useState } from 'react';
    // TODO
  }


  //histroy是一个数组
  const moves = history.map((squares, move) => {
    let description;
    if (move > 0) {
      description = 'Go to move #' + move;
    } else {
      description = 'Go to game start';
    }
    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;
}

在你可以实现 jumpTo 之前,你需要 Game 组件来跟踪用户当前正在查看的步骤。为此,定义一个名为 currentMove 的新 state 变量,默认为 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];
  //...
}

接下来,更新 Game 中的 jumpTo 函数来更新 currentMove。如果你将 currentMove 更改为偶数,你还将设置 xIsNexttrue

export default function Game() {
  // ...
  function jumpTo(nextMove) {
    setCurrentMove(nextMove);
    setXIsNext(nextMove % 2 === 0);
  }
  //...
}

你现在将对 GamehandlePlay 函数进行两处更改,该函数在你单击方块时调用。

  • 如果你“回到过去”然后从那一点开始采取新的行动,你只想保持那一点的历史。不是在 history 中的所有项目(... 扩展语法)之后添加 nextSquares,而是在 history.slice(0, currentMove + 1) 中的所有项目之后添加它,这样你就只保留旧历史的那部分。
  • 每次落子时,你都需要更新 currentMove 以指向最新的历史条目。
function handlePlay(nextSquares) {
  const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
  setHistory(nextHistory);
  setCurrentMove(nextHistory.length - 1);
  setXIsNext(!xIsNext);
}

最后,你将修改 Game 组件以渲染当前选定的着法,而不是始终渲染最后的着法:

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];
    //修改了这里 而不是 = history[history.length - 1];

}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值