【React】公式チュートリアルやってみた

Reactの公式ページにはチュートリアルが用意されています。

今回はコチラをTypescriptを使ってやっていきます。(元はJavascript)

環境構築

チュートリアルを実施する場所として、今回はローカルの開発環境を用意しました。

詳しくは以下の記事をご覧ください。

プロジェクト作成

チュートリアル用のプロジェクトを作成します。

$ yarn create react-app tutorial --template typescript

チュートリアル

コンポーネントの分割など、少し順番が違うところがあるかもしれませんがご了承を。

盤面の表示まで

srcフォルダ配下にcomponentsフォルダを作成し、「Board.tsx」および「Square.tsx」ファイルを作成します。

.
├── node_modules
├── public
└── src/
    └── components/
        ├── Board.tsx
        └── Square.tsx

それぞれのファイル内容は以下の通り。

Board.tsx

import React from 'react';
import '../App.css';
import Square from './Square';

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

export default Board;

Square.tsx

import React from 'react';
import '../App.css';

type Props = {
  value: number
}

const Square = (props: Props) => {
  return (
    <button className="square">{props.value}</button>
  );
}

export default Square;

最後にsrc配下のApp.tsxを編集します。

App.tsx

import React from 'react';
import './App.css';
import Board from './componets/Board';

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

export default App;

propsの渡し方に少しハマりました。

Typescriptならではの型指定。

実行結果

スタイルシートの適用を忘れていました…

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

* {
  box-sizing: border-box;
}

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

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

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

.status {
  margin-bottom: 10px;
}
.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

チュートリアルから飛べるCodeSandboxより。

マス目クリックでXが表示されるところまで

Square.tsx

import { useState } from 'react';
import React from 'react';
import '../App.css';

function Square() {

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

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

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

export default Square;

Board.tsx

import React from 'react';
import '../App.css';
import Square from './Square';

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

export default Board;

Suqare.tsxの書き方に少しハマりました。入れ子なんですね…。

実行結果

マス目の制御をBoard.tsxに移管

Square.tsx

import { MouseEventHandler } from 'react';
import React from 'react';
import '../App.css';

type Props = {
  value: string,
  onSquareClick: MouseEventHandler<HTMLButtonElement>
}

const Square = (props: Props) => {
  return (
    <button className="square" onClick={props.onSquareClick}>{props.value}</button>
  );
}

export default Square;

Board.tsx

import React from 'react';
import { useState } from 'react';
import '../App.css';
import Square from './Square';

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

  function handleClick(i: number) {
    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>
    </>
  );
}

export default Board;

クリックイベントの型と、関数の渡し方に注意。

実行結果

前項と変わりないので割愛。

空のマス目に交互にXとOを埋めていくところまで

Board.tsx

import React from 'react';
import { useState } from 'react';
import '../App.css';
import Square from './Square';

function Board() {

  /** XとOの入力順序制御 */
  const [xIsNext, setXIsNext] = useState(true);
  /** マス目のデータ */
  const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i: number) {

    /** マス目が埋まっているかの確認 */
    if (squares[i]) {
      return;
    }

    const nextSquares = squares.slice();

    /** XとOを交互に埋めていく */
    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>
    </>
  );
}

export default Board;

一部コメントも追加しました。

実行結果

Gameコンポーネントの追加まで

src/components配下にGame.tsxファイルを追加します。

.
├── node_modules
├── public
└── src/
    └── components/
        ├── Board.tsx
        ├── Game.tsx(New!!)
        └── Square.tsx

Game.tsx

import React from 'react';
import { useState } from 'react';
import '../App.css';
import Board from './Board';

function Game() {


  /** XとOの入力順序制御 */
  const [xIsNext, setXIsNext] = useState(true);
  /** マス目のデータ */
  const [history, setHistory] = useState([Array(9).fill(null)]);
  const currentSquares = history[history.length - 1];

  function handlePlay(nextSquares: Array<string>) {
    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>
  );
}

export default Game;

Board.tsx

import React from 'react';
import '../App.css';
import Square from './Square';

type Props = {
  xIsNext: boolean,
  squares: Array<string>,
  onPlay: any
}

const Board = (props:Props) => {

  /**
   * 現在のプレイヤー表示およびゲーム結果の表示
   */
  const winner = calculateWinner(props.squares);
  let status;
  if (winner) {
    status = "Winner: " + winner;
  } else {
    status = "Next player: " + (props.xIsNext ? "X" : "O");
  }

  function handleClick(i: number) {

    /** 
     * マス目が埋まっているか、
     * またはゲームが終了しているかの確認
     */
    if (props.squares[i] || calculateWinner(props.squares)) {
      return;
    }

    const nextSquares = props.squares.slice();

    /** XとOを交互に埋めていく */
    if (props.xIsNext) {
      nextSquares[i] = "X";
    } else {
      nextSquares[i] = "O";
    }
    props.onPlay(nextSquares);
  }

  return (
    <>
      <div className="status">{status}</div>
      <div className="board-row">
        <Square value={props.squares[0]} onSquareClick={() => handleClick(0)} />
        <Square value={props.squares[1]} onSquareClick={() => handleClick(1)} />
        <Square value={props.squares[2]} onSquareClick={() => handleClick(2)} />
      </div>
      <div className="board-row">
        <Square value={props.squares[3]} onSquareClick={() => handleClick(3)} />
        <Square value={props.squares[4]} onSquareClick={() => handleClick(4)} />
        <Square value={props.squares[5]} onSquareClick={() => handleClick(5)} />
      </div>
      <div className="board-row">
        <Square value={props.squares[6]} onSquareClick={() => handleClick(6)} />
        <Square value={props.squares[7]} onSquareClick={() => handleClick(7)} />
        <Square value={props.squares[8]} onSquareClick={() => handleClick(8)} />
      </div>
    </>
  );
}

function calculateWinner(squares: Array<string>) {
  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;
}

export default Board;

Square.tsx

import React from 'react';
import '../App.css';

type Props = {
  value: string,
  onSquareClick: any
}

const Square = (props: Props) => {
  return (
    <button className="square" onClick={props.onSquareClick}>{props.value}</button>
  );
}

export default Square;

型指定はanyでも大丈夫なんですね…。

実行結果

前項と変わりないので割愛。

完成

src/components配下にMove.tsxファイルを追加します。

コード量とかを考えるとGame.tsxの中にあっても良いかと思ったのですが、せっかくチュートリアルなので1ファイル1コンポーネントを意識してみました。

.
├── node_modules
├── public
└── src/
    └── components/
        ├── Board.tsx
        ├── Game.tsx
        ├── Move.tsx(New!!)
        └── Square.tsx

Game.tsx

import React from 'react';
import { useState } from 'react';
import '../App.css';
import Board from './Board';
import Move from './Move';

function Game() {

  /** マス目のデータ */
  const [history, setHistory] = useState([Array(9).fill(null)]);
  /** 何手目であるか */
  const [currentMove, setCurrentMove] = useState(0);
  /** 現在の盤面 */
  const currentSquares = history[currentMove];

  /** XとOの入力順序制御 */
  const xIsNext = currentMove % 2 === 0;

  function handlePlay(nextSquares: Array<string>) {
    const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
    setHistory(nextHistory);
    setCurrentMove(nextHistory.length - 1);
  }

  function jumpTo(nextMove: number) {
    setCurrentMove(nextMove);
  }

  return (
    <div className="game">
      <div className="game-board">
        <Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
      </div>
      <div className="game-info">
        <ol>
          <Move history={history} onClickMove={jumpTo} />
        </ol>
      </div>
    </div>
  );
}

export default Game;

Move.tsx

import React from 'react';
import '../App.css';

type Props = {
  history: Array<Array<string>>,
  onClickMove: any
}

const Move = (props: Props) => {

  return (
    <>
      {props.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={() => props.onClickMove(move)}>{description}</button>
          </li>
        );
      })}
    </>
  );
}

export default Move;

「ループ処理のトコ、どう書けばいいんじゃ!」

ってなりましたが、意外とすんなり書けました。

実行結果

所感

最後に、チュートリアルを通して感じたことを少し。

Reactの基本ともいうべきコンポーネントという概念について。

これ、マジで気に入ってます。単体テストが楽になるんじゃないでしょうか。

オブジェクト指向とかマイクロサービスとか、そういうカタチのフロント版ってイメージ。

コンポーネントの作り方にセンスが問われそう?

実務で携わるのが楽しみです。

以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA