[React][typescript]官方教程小游戏Tic-Tac-Toe

本文记录一些跟随官方教程后的心得,按照最终版本代码逐块进行理解,包括typescript和react的一些基本操作和误区

函数组件

  • Game: 最后输出的大组件,所有组件的父类
  • Board:Game中引用的组件,包含boardProps类存储属性,函数组件Board
  • Square:Board中引用的组件,包含squareProps类存储属性,函数组件Square
  • 函数findWinner()

细节知识

创建函数组件并传入参数
Syntax

function funcName({value1, value2, ...}:funcProps) {...}
or
const funcName: React.FC<funcProps> = ({value1, value2, ...}) => {...}

Note
  • funcProps是用来存储传入参数的interface,通常创建在函数组件之前, i.e
    interface funcProps {value1:string; value2: React.MouseEventHandler<HTMLButtonElement>};

  • 一些函数的类型可以在IDE的tips里可以看到。比如创建一个handleClick函数,在点击时候引用(i.e. onclick={handleClick}),当鼠标悬于onclick之上时,IDE如vs code会显示onclick函数的类型React.MouseEventHandler<HTMLButtonElement>

()=>arrow function箭头函数

在本例中,因为handleClick()需要传入参数i,并且Board中引用Square时就会传入这个函数属性,导致渲染时该函数就会被呼叫并重新渲染页面,此时react会抛出警告。解决方法是在函数再套一个函数,这里使用箭头函数。
<Square value={squares[3]} handleSquareClick={()=>handleClick(3)} />
该解决方法适用于大部分带有参数的函数属性。

useState Hook in Typescript
Syntax

const [property, setProperty] = useState<type>(default value)

示例
  • number类型
    const [currentMove, setCurrentMove] = useState<number>(0);
  • array
    const [history, setHistory] = useState<string[]>(Array(3).fill(null));
    react会创建一个长度为3的string组,默认初始值为null。i.e.{null, null, null}
  • 2d array矩阵
    const [history, setHistory] = useState<string[][]>([Array(3).fill(null)]);
    这里,react会创建一个包含长度为3的string数组的数组,也就是矩阵。初始默认值是只有一个数组。i.e.{[null, null, null] --> [Array(3).fill(null)]};随着程序进行,会有新的数组被创建并加入, i.e. {[null, null, null], [null, null, null], ...}
array的map函数
Syntax

array.map((element) => ...)
array.map((element, index) => ...)
array.map((element, index, array) => ...)

react中的list元素

根据官方的教程推荐,JSX/TSX中的list元素应该具备一个唯一的key,i.e.
<ol><li key={keyVal}> ... </li></ol>

Game组件

Game作为App组件输出的最大的组件,其作用为控制整个游戏流程,因此Board和Square只负责渲染棋盘和控制棋子,Game负责监控每一次move并记录,以及判断胜利者-->findWinner()。在这里复盘一下游戏中时间回溯的功能。

变量&函数
  • currentMove:number,游戏中的move是指玩家目前为止移动的总步数,默认为0,即游戏开始未移动时;
  • isXNext:boolean,判断下一步是X还是O。因为这两个状态是交替变换的,因此不需要hook,直接使用(currentMove % 2 === 0)即可,currentMove为双数(包括0)时下一步是X,否则为O;
  • history:string[ ][ ],矩阵。每下一步history就会将当前的棋盘状态加在末尾,初始状态只有一个长度为9的全为null值的列表,即currentMove=0时棋盘的状态;
  • currentSquare:string[ ],当currentMove = i时,history[i] = currentSquare,即当时的棋盘状态
  • handlePlay(newSquare):函数,此函数最后会被传入Board组件并由Board来更新其中的newSquare。利用newSquare来创建一个新的history --> newhistory是由原先的加上newSquare,然后更新history,最后更新currentMove,考虑到有时间回溯的功能,所以不能单纯+=1,而是要将当前步数设置为history的最大目录数

源码

点击查看代码

import { useState } from "react";

interface squareProps {
  value: string;
  handleSquareClick: React.MouseEventHandler<HTMLButtonElement>;
}
// function component Square
function Square({value, handleSquareClick} : squareProps) {
  
  // syntax: const [property, setProperty] = useState<type>(default value)
  //const [value, setValue] = useState<string>("");

  //function handleClick() {
  //  console.log("Clicked!");
  //}

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

interface boardProps {
  isXNext:boolean;
  squares:string[];
  onPlay: Function;
}

// export the game board
function Board({isXNext, squares, onPlay}:boardProps) {

  // when rendering the board
  const winner = findWinner(squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  }
  else {
    status = "Next Player: " + (isXNext? "X":"O");
  }

  function handleClick(i:number) {
    // check if a. squares[i] is already filled / b. a winner has been declared
    if (squares[i] || findWinner(squares)) {
      return;
    }

    const newSquares = squares.slice(); // create a copy of the current squares array
    if (isXNext) {
      newSquares[i] = "X";                // update the value
    } 
    else {
      newSquares[i] = "O";
    }
    onPlay(newSquares);
  }

  // ()=> is the syntax of arrow function
  // passing properties using ()=> to avoid calling function before clicking
  // which means to place the handler inside a function
  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={squares[0]} handleSquareClick={()=>handleClick(0)} />
        <Square value={squares[1]} handleSquareClick={()=>handleClick(1)} />
        <Square value={squares[2]} handleSquareClick={()=>handleClick(2)} />
      </div>

      <div className="board-row">
        <Square value={squares[3]} handleSquareClick={()=>handleClick(3)} />
        <Square value={squares[4]} handleSquareClick={()=>handleClick(4)} />
        <Square value={squares[5]} handleSquareClick={()=>handleClick(5)} />      
      </div>

      <div className="board-row">
        <Square value={squares[6]} handleSquareClick={()=>handleClick(6)} />
        <Square value={squares[7]} handleSquareClick={()=>handleClick(7)} />
        <Square value={squares[8]} handleSquareClick={()=>handleClick(8)} />
      </div>
    </>
  );
}

export default function Game() {
  // syntax: const [property, setProperty] = useState<type>(default value)
  const [currentMove, setCurrentMove] = useState<number>(0);
  const isXNext = currentMove % 2 === 0;
  // default value to history array: an empty array [] containg 9 null value -> at game start
  const [history, setHistory] = useState<string[][]>([Array(9).fill(null)]);
  const currentSquare = history[currentMove];

  function handlePlay(newSquares:string[]) {
    const newHistory = [...history.slice(0, currentMove+1), newSquares];
    setHistory(newHistory);                 // update the history array
    setCurrentMove(newHistory.length - 1);  // update the current move
  }

  function jumpTo(move:number) {
    setCurrentMove(move);                   //update the current move
  }

  console.log(history);
  // syntax: array.map((element, index) => ...)
  const moves = history.map((squares, move) => {
    let info;
    if (move > 0) {
      info = "Go to Move#" + move;
    } else {
      info = "Go to Start";
    }
    // notice: assigning a key to each list element
    return (
      <li key={move}>
        <button onClick={()=>jumpTo(move)}>{info}</button>
      </li>
    );
  });

  return (
    <div className="game-play">
      <div className="game-board">
        <Board isXNext={isXNext} squares={currentSquare} onPlay={handlePlay}/>
      </div>
      <div className="game-history">
        <ol>{moves}</ol>
      </div>
    </div>
  );
}


function findWinner(squares:string[]) {
  // define the winning lines
  const winLines = [
    [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<winLines.length; ++i) {
    const [a, b, c] = winLines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return "";
}

posted @ 2023-03-13 15:04  Akira300000  阅读(64)  评论(0编辑  收藏  举报