12/08/2018, 15:31

Nhập môn React với TicTacToe - Phần 1

Bài viết chỉ dành cho những ai vừa mới tập học ReactJS. Bài viết dựa trên ví dụ TicTacToe trên trang chủ của ReactJS (https://facebook.github.io/react/tutorial/tutorial.html) Link demo mẫu của bài viết này:(https://codepen.io/NguyenHoangAnhDung/pen/PjmRQR?editors=0010) 1. Mục đích: Tạo ...

  • Bài viết chỉ dành cho những ai vừa mới tập học ReactJS.
  • Bài viết dựa trên ví dụ TicTacToe trên trang chủ của ReactJS (https://facebook.github.io/react/tutorial/tutorial.html)
  • Link demo mẫu của bài viết này:(https://codepen.io/NguyenHoangAnhDung/pen/PjmRQR?editors=0010)

1. Mục đích:

  • Tạo project ReactJS.
  • Tạo các class để quản lý game TicTacToe.

2. Thực hiện: a. Tạo project ReactJS như sau:

npm install -g create-react-app
create-react-app my-app

cd my-app
npm start

Khi truy cập vào http://localhost:3000, kết quả như sau: b. Tạo các class để quản lý game TicTacToe: Giống với trên Tutorial của ReactJS, game TicTacToe sẽ chia làm 3 class:

  • Game: Là class cha của bài toán, có nhiệm vụ lưu trữ các giá trị do người dùng tạo ra khi chơi game. Gọi đến Board.
  • Board: Có nhiệm vụ gọi đến Square để hiển thị các ô vuông trong trò chơi.
  • Square: Mỗi Square là một ô vuông trên màn hình.

Hình minh họa: Sau khi đã phân tích các class cần có cho bài toán, chúng ta thêm các class vào code như sau:

  • Xóa thư mục my-app/src/ để thay thế bằng các tệp JS và CSS mới.
  • Tạo mới thư mục my-app/src và thêm vào 2 tệp index.css và index.js.
  • Coppy nội dung sau vào index.css (Code CSS này của React nhé: https://facebook.github.io/react/tutorial/tutorial.html)
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.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;
  awidth: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}
  • Coppy nội dung sau vào index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Game extends React.Component{
  render(){
    return(
      <div>Game</div>
    );
  }
}

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

Đến đây thì bạn được kết quả như sau: Việc tiếp theo là thêm 3 class Game, Board, Square vào:

  • Thêm đoạn code sau vào index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component{
  render(){
    return(
      <div>Square</div>
    );
  }
}

class Board extends React.Component{
  render(){
    return(
      <div>Board</div>
    );
  }
}

class Game extends React.Component{
  render(){
    return(
      <div>Game</div>
    );
  }
}

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
  • Như đã nói, Game sẽ gọi Board, còn Board sẽ gọi Square, vì vậy ta thay đổi nội dung của Game và Board như sau:
class Board extends React.Component{
  render(){
    return(
      <div>
        <div>Board</div>
        <div><Square /></div>
      </div>
    );
  }
}

class Game extends React.Component{
  render(){
    return(
      <div>
        <div>Game</div>
        <div><Board /></div>
      </div>
    );
  }
}

Kết quả như sau: Lưu ý nếu đoạn code trên thay bằng:

class Game extends React.Component{
  render(){
    return(
        <div>Game</div>
        <div><Board /></div>
    );
  }
}

thì sẽ xảy ra lỗi, như trên thì ta đang return 2 <div> mà React bắt buộc class khi return phải return một phần tử duy nhất (div, table, span, ...). Như hiện tại thì tất cả các class đều nằm chung trong index.js, chúng ta nên tách chúng ra thành nhiều phần phòng trường hợp class phình to ra:

  • Tạo 3 tệp tin Game.js, Board.js, Square.js như sau:
  • Thay đổi nội dung index.js bằng:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Game from './components/Game';

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
  • Nội dung Game.js:
import React from 'react';
import Board from './Board';

class Game extends React.Component{
  render(){
    return(
      <div>
        <div>Game</div>
        <div><Board /></div>
      </div>
    );
  }
}

export default Game;
  • Nội dung Board.js:
import React from 'react';
import Square from './Square';

class Board extends React.Component{
  render(){
    return(
      <div>
        <div>Board</div>
        <div><Square /></div>
      </div>
    );
  }
}

export default Board;
  • Nội dung Square.js:
import React from 'react';

class Square extends React.Component{
  render(){
    return(
      <div>Square</div>
    );
  }
}

export default Square;

Như vậy ta đã hoàn thành các mục tiêu sau:

  • Khởi tạo ReactJS app.
  • Định nghĩa các class cần có.
  • Thêm các class vào trong code.

1. Mục đích:

  • Hiển thị bảng chơi TicTacToe.
  • Cho phép User chơi và tính toán người chiến thắng.
  • Hiển thị lịch sử kèm với vị trí đã chọn.
  • Đánh dấu dòng thắng cuộc.
  • Sắp xếp danh sách theo thứ tự từ nhỏ đến lớn hoặc ngược lại.

2. Thực hiện: a. Hiển thị bảng chơi TicTacToe Bài toán của chúng ta sẽ dùng ma trận 3x3 như trong ví dụ của ReactJS. Chúng ta sẽ bắt đầu từ Game -> gọi đến Board và Board sẽ gọi 9 ô Square -> mỗi Square sẽ return lại một ô vuông. Do đó, cần phải viết phương thức gọi Square cho Board và một vòng lặp để gọi 9 ô Square thay vì gọi lần lượt 9 lần (Sẽ đề cập sau).

  • Thêm nội dung sau vào Board.js:
import React from 'react';
import Square from './Square';

class Board extends React.Component{
 // Phương thức này trả về class Square kèm giá trị đi theo là value ( Giá trị `value` trong ReactJS được gọi là props. Tìm hiểu thêm tại đây: https://facebook.github.io/react/docs/components-and-props.html)
  renderSquare(i){
    return <Square value={i} />
  }

  render(){
    return(
      <div>
        <div>Board</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

export default Board;
  • Thay đổi nội dung Square.js để trả về một ô vuông:
import React from 'react';

class Square extends React.Component{
  render(){
    return(
      <button className="square">{this.props.value}</button> // this.props.value ở đây là giá trị i bên renderSquare(i) của Board
    );
  }
}

export default Square;

Kết quả như sau: Tuy nhiên, trong bài toán này chỉ là ma trận 3x3, nếu yêu cầu bài toán là ma trận 10x10 hoặc 100x100 thì việc gọi Square thủ công sẽ không hay. Do đó ta cần vòng lặp để gọi Square theo kích thước của ma trận:

  • Thay đổi nội dung Game.js:
import React from 'react';
import Board from './Board';

class Game extends React.Component{
  constructor(){
    super();
    this.state = {
      squares: Array(9).fill(null),
    }; // Khởi tạo Game với state là một Array gồm 9 phần tử null
  }

  render(){
    return(
      <div>
        <div className="game"><Board squares={this.state.squares} /></div> // Gửi state squares đó qua cho Board
      </div>
    );
  }
}

export default Game;
  • Thay đổi nội dung Board.js:
import React from 'react';
import Square from './Square';

class Board extends React.Component{
  renderSquare(i){
    return <Square value={i} />
  }

  render(){
    const matrixSize = Math.sqrt(this.props.squares.length); // Lấy kích cỡ của ma trận bằng props gửi từ Game qua
    const rows = Array(matrixSize).fill(null); // Tạo rows là một Array để tiện sử dụng hàm map()
    const cols = rows; // Ma trận vuông nên cols = rows
    const board = rows.map((row, i) => {
      const squares = cols.map((col, j) => {
        const squareKey = i * matrixSize + j;
        return <span key={squareKey}>{this.renderSquare(squareKey)}</span>; // Chúng ta đang sử dụng vòng lặp trong reactJS nên phải có key cho mỗi phần tử (https://facebook.github.io/react/docs/lists-and-keys.html)
      });
      return <div className="board-row" key={i}>{squares}</div> // Tương tự như trên
    });
    return(
      <div>
        <div>Board</div>
        <div>{board}</div>
      </div>
    );
  }
}

export default Board;
  • Đoạn code render các phần tử tương tự như đoạn code sau:
import React from 'react';
import Square from './Square';

class Board extends React.Component{
  renderSquare(i){
    return <Square value={i} />
  }

  renderAllSquares(){
    const matrixSize = Math.sqrt(this.props.squares.length);
    const board = Array(matrixSize).fill(null);
    for(let i = 0; i < matrixSize; i++){
        const squares = Array(matrixSize).fill(null);
        for(let j = 0; j < matrixSize; j++){
            var squareKey = i * matrixSize + j;
            squares.push(<span key={squareKey}>{this.renderSquare(squareKey)}</span>);
        }
        board.push(<div key={i}>{squares}</div>);
    }
    return board;
  }

  render(){
    return(
      <div>
        <div>Board</div>
        <div>{this.renderAllSquares()}</div>
      </div>
    );
  }
}

export default Board;
  • Thay đổi nội dung phương thức 'renderSquare' trong Board.js để hiển thị trạng thái ban đầu của bảng TicTacToe:
renderSquare(i){
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)}/>
}

Đến đây chúng ta đã xong phần hiển thị bảng TicTacToe rồi. b. Cho phép User chơi và tính toán người chiến thắng Về phần thuật toán để tính toán người chiến thắng thì chúng ta sẽ sử dụng luôn thuật toán của ReactJS trong tutorial. Đầu tiên, ta tạo sự kiện cho mỗi ô vuông trong bảng bằng cách:

Định nghĩa phương thức 'handleClick' cho sự kiện onClick trong Game. Gửi 'handleClick' đó qua Board. Board gửi tiếp qua Square.

  • Thêm nội dung sau vào Game.js:
import React from 'react';
import Board from './Board';

class Game extends React.Component{
  constructor(){
    super();
    this.state = {
      squares: Array(9).fill(null),
    };
  }
  
  handleClick(i){
    console.log(i);
  }

  render(){
    return(
      <div>
        <div className="game"><Board squares={this.state.squares} onClick={i => this.handleClick(i)} /></div>  // Định nghĩa hàm handleClick và gửi nó qua cho Board
      </div>
    );
  }
}

export default Game;
  • Thêm onClick vào renderSquare trong Board.js:
renderSquare(i){
    return <Square value={this.props.squares[i]} onClick={() => this.props.onClick(i)}/>
}
  • Thêm onClick vào renderSquare trong Board.js:
<button className="square" onClick={this.props.onClick}>{this.props.value}</button>

Lúc này, ta thử click vào một ô bất kỳ trên màn hình và thấy handleClick trong class Game đã hoạt động. V Việc tiếp theo của chúng ta là cho người dùng 'X' và 'O' tương ứng chơi, ở đây giả sử người chơi 'X' bắt đầu trước:

Tạo cờ xIsNext: nếu true thì tiếp theo là 'X', false thì tiếp theo là 'O'. Update sự kiện onClick: mỗi khi click sẽ update cờ xIsNext thành ngược lại và nội dung ô được click sẽ tương ứng với cờ xIsNext.

  • Thêm nội dung sau vào Game.js:
import React from 'react';
import Board from './Board';

class Game extends React.Component{
  constructor(){
    super();
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i){
    console.log(i);
  }

  render(){
    const status = "Next player is: " + (this.state.xIsNext ? 'X' : 'O');
    return(
      <div>
        <div className="game"><Board squares={this.state.squares} onClick={i => this.handleClick(i)} /></div>
        <div className="game-info">
          <p>{status}</p>
        </div>
      </div>
    );
  }
}

export default Game;

Kết quả như sau: Bây giờ chúng ta sẽ thêm phần thuật toán tính toán người thắng cuộc (lấy của ReactJS tutorial), với các nhiệm vụ sau:

Không thể thay đổi nội dung ô đã click (tức là không được click 2 lần). Tính toán người chiến thắng. Khi có người chiến thắng sẽ không click vào được nữa. Hiển thị người thắng cuộc Tính toán trường hợp hòa.

  • Hiện tại thì ta có thể click 2 lần vào 1 ô và giá trị đó thay đổi theo từng lần click, do đó thêm nội dung sau vào handClick trong Game.js:
handleClick(i){
    const squares = this.state.squares.slice();
    if(squares[i]){
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext
    });
  }
  • Thêm thuật toán sau vào dưới hoặc trên class Game.js:
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;
}
  • Để tính toán trường hợp hòa, ta phải thêm một trường stepNumber vào như sau:
import React from 'react';
import Board from './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;
}

class Game extends React.Component{
  constructor(){
    super();
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
      stepNumber: 0, // Khởi tạo stepNumber là 0
    };
  }

  handleClick(i){
    const squares = this.state.squares.slice();
    if(calculateWinner(squares) || squares[i]){
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
      stepNumber: this.state.stepNumber + 1 // Update stepNumber += 1 mỗi lần click l
    });
  }

  render(){
    const squares = this.state.squares.slice();
    const winner = calculateWinner(squares);
    let status;
    if(winner){
      status = "Winner is: " + winner; // Nếu winner có giá trị thì sẽ hiển thị người thắng cuộc
    }else if(this.state.stepNumber === 9){ // Nếu sau 9 lần chưa có ai win thì hòa
      status = "No one win"; 
    }else{
      status = "Next player is: " + (this.state.xIsNext ? 'X' : 'O');
    }
    return(
      <div>
        <div className="game"><Board squares={squares} onClick={i => this.handleClick(i)} /></div>
        <div className="game-info">
          <p>{status}</p>
        </div>
      </div>
    );
  }
}

export default Game;

Đến đây ta đã hoàn thành mục đích cho phép User chơi và tính toán người chiến thắng lẫn người hòa rồi. Như vậy từ đầu đến giờ chúng ta đã thành công trong:

Hiển thị bảng chơi TicTacToe. Cho phép User chơi và tính toán người chiến thắng.

Các chức năng còn lại sẽ được đề cập đến ở phần 2 (https://viblo.asia/p/nhap-mon-react-voi-tictactoe-phan-2-Do754N34ZM6), xin cảm ơn các bạn đã theo dõi!

0