React公式チュートリアルをTypeScriptでやる

TL; DR (Too long didn't read, 要約)

Reactの公式チュートリアル を TypeScript を使って実装

・ソースコード


構築

今回は TypeScript を使うため、公式チュートリアルにある以下のコマンドではなく、

$ npx create-react-app my-app

Create React App 公式 にあるコマンドを使ってプロジェクトを生成します。ついでに、TypeScriptで用いる「型」関連のライブラリをインストールします。また、GitHubリポジトリにpushした後に、アプリを起動してみます。

$ mkdir -p workspace/private
$ cd workspace/private
$ npx create-react-app react-tutorial-with-ts --typescript
npx: installed 91 in 11.001s

Creating a new React app in /Users/tkugimot/workspace/private/react-tutorial-with-ts.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts...
...

$ cd react-tutorial-with-ts
$ npm install --save typescript @types/node @types/react @types/react-dom @types/jest
$ git add .
$ git commit -m 'Add type lib'
$ git remote add origin git@github.com:tkugimot/react-tutorial-with-ts.git
$ git push origin master
$ npm start

 localhost:3000 がブラウザで開いて、以下のような画面が表示されます。


コーディングに入る前に、適当な Editor/IDE で react-tutorial-with-ts ディレクトリを開きます。僕は Inttelij の使用感が大好きなので今回も Intellij (Ultimate) を使います。別に Visual Studio Code でも Vim でも Atom でも何でも良いと思います。以下にある通りに設定しておくと良さそうです。

https://babeljs.io/docs/en/editors/


コンポーネントを作る

基本的に、

1. まず公式チュートリアルの通りに実装

2. TypeScriptに書き直すことでコンパイルエラーを解決

という手順でやっていこうと思います。

まず、create-react-app で生成された /src 以下のコードを全て削除して、更な状態に戻します。

$ rm -rf src/*

次に、公式にある通りに src/index.css をコピペして追加します。

$ touch src/index.css

# 以下を index.css に追加

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

.square:focus {
 outline: none;
}

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

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

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

次に、src/index.tsx を追加して、公式通りに index.js をコピペします。

$ touch src/index.tsx

# 以下のコードを追加
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
   render() {
       return (
           <button className="square">
               {/* TODO */}
           </button>
       );
   }
}

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

   render() {
       const status = 'Next player: X';

       return (
           <div>
               <div className="status">{status}</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>
       );
   }
}

class Game extends React.Component {
   render() {
       return (
           <div className="game">
               <div className="game-board">
                   <Board />
               </div>
               <div className="game-info">
                   <div>{/* status */}</div>
                   <ol>{/* TODO */}</ol>
               </div>
           </div>
       );
   }
}

// ========================================

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


すると、早速 TypeScript によって以下のコンパイルエラーが起きています。これは嬉しいことです。

Parameter 'i' implicitly has an 'any' type.
# src/index.tsx

renderSquare(i) {
    return <Square />;
}

以下のように修正します。i に number 型情報を付与してあげます。

-    renderSquare(i) {
+    renderSquare(i: number) {
        return <Square />;
    }


これでコンパイルエラーが消えたので、また npm start してみます。すると、以下のような画面が表示されます。


次に、Props関連のコードを追加します。

diff --git a/src/index.tsx b/src/index.tsx
index 420dcc9..c4b5825 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,7 +6,7 @@ class Square extends React.Component {
    render() {
        return (
            <button className="square">
-                {/* TODO */}
+                {this.props.value}
            </button>
        );
    }
@@ -14,7 +14,7 @@ class Square extends React.Component {

class Board extends React.Component {
    renderSquare(i: number) {
-        return <Square />;
+        return <Square value={i} />;
    }

    render() {
Property 'value' does not exist on type 'Readonly<{}> & Readonly<{ children?: ReactNode; }>'. TS2339

TypeScript の場合、プロパティの定義には Interface が必要です。

これを以下のように書き直します。

diff --git a/src/index.tsx b/src/index.tsx
index c4b5825..8941c58 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,7 +2,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

-class Square extends React.Component {
+interface SquarePropsInterface {
+    value: number;
+}
+
+class Square extends React.Component<SquarePropsInterface> {
    render() {
        return (
            <button className="square">


次に、クリックした Square の表示を 'X' に書き換えるために、Stateを用いて以下のように書きます。

diff --git a/src/index.tsx b/src/index.tsx
index 8941c58..dbd2af6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -7,10 +7,20 @@ interface SquarePropsInterface {
}

class Square extends React.Component<SquarePropsInterface> {
+    constructor(props: SquarePropsInterface) {
+        super(props);
+        this.state = {
+            value: null,
+        };
+    }
+
    render() {
        return (
-            <button className="square">
-                {this.props.value}
+            <button
+                className="square"
+                onClick={() => this.setState({value: 'X'})}
+            >
+                {this.state.value}
            </button>
        );
    }
Property 'value' does not exist on type 'Readonly<{}>'. TS2339

もうお気づきかと思いますが、Stateの定義にも Interface が必要です。

diff --git a/src/index.tsx b/src/index.tsx
index dbd2af6..6f2ba92 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,11 +6,15 @@ interface SquarePropsInterface {
    value: number;
}

-class Square extends React.Component<SquarePropsInterface> {
+interface SquareStateInterface {
+    value: string;
+}
+
+class Square extends React.Component<SquarePropsInterface, SquareStateInterface> {
    constructor(props: SquarePropsInterface) {
        super(props);
        this.state = {
-            value: null,
+            value: "",
        };
    }


ここまでのソースコード全体を載せておきます。

# src/index.tsx 
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquarePropsInterface {
   value: number;
}

interface SquareStateInterface {
   value: string;
}

class Square extends React.Component<SquarePropsInterface, SquareStateInterface> {
   constructor(props: SquarePropsInterface) {
       super(props);
       this.state = {
           value: "",
       };
   }

   render() {
       return (
           <button
               className="square"
               onClick={() => this.setState({value: 'X'})}
           >
               {this.state.value}
           </button>
       );
   }
}

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

   render() {
       const status = 'Next player: X';

       return (
           <div>
               <div className="status">{status}</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>
       );
   }
}

class Game extends React.Component {
   render() {
       return (
           <div className="game">
               <div className="game-board">
                   <Board />
               </div>
               <div className="game-info">
                   <div>{/* status */}</div>
                   <ol>{/* TODO */}</ol>
               </div>
           </div>
       );
   }
}

// ========================================

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


OXゲームを完成させる

これ以降は diff を示さずに、TypeScriptのコードのみ貼っていきます。基本的には、Props と State の inteface を定義した上で型をつけながらチュートリアル通りに実装していくだけです。

一旦、Boardのstateで状態を管理します。

# src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquarePropsInterface {
   value: string;
   onClick: () => void
}

interface SquareStateInterface {
   value: string;
}

class Square extends React.Component<SquarePropsInterface, SquareStateInterface> {
   constructor(props: SquarePropsInterface) {
       super(props);
       this.state = {
           value: "",
       };
   }

   render() {
       return (
           <button
               className="square"
               onClick={() => this.props.onClick()}
           >
               {this.props.value}
           </button>
       );
   }
}

interface BoardPropsInterface {
   squares: Array<string>
}

interface BoardStateInterface {
   squares: Array<string>
}

class Board extends React.Component<BoardPropsInterface, BoardStateInterface> {
   constructor(props: BoardPropsInterface) {
       super(props);
       this.state = {
           squares: Array(9).fill(""),
       };
   }

   handleClick(i: number) {
       console.log(i);
       const squares: Array<string> = this.state.squares.slice();
       squares[i] = 'X';
       this.setState({
           squares: squares
       });
       console.log(this.state);
   }

   renderSquare(i: number) {
       return <Square
           value={this.state.squares[i]}
           onClick={() => this.handleClick(i)}
       />;
   }

   render() {
       const status = 'Next player: X';

       return (
           <div>
               <div className="status">{status}</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>
       );
   }
}

class Game extends React.Component {
   render() {
       return (
           <div className="game">
               <div className="game-board">
                   <Board  squares={Array(9).fill("")}/>
               </div>
               <div className="game-info">
                   <div>{/* status */}</div>
                   <ol>{/* TODO */}</ol>
               </div>
           </div>
       );
   }
}

// ========================================

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


次に、Square を関数コンポーネントに書き換えます。

# src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquarePropsInterface {
   value: string;
   onClick: () => void
}

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


手番の処理を追加します。Boardのstateに xIsNext という状態を持たせ、これが true の場合に 'X' を描画するように修正します。

class Board extends React.Component<BoardPropsInterface, BoardStateInterface> {
   constructor(props: BoardPropsInterface) {
       super(props);
       this.state = {
           squares: Array(9).fill(""),
           xIsNext: true
       };
   }

   handleClick(i: number) {
       console.log(i);
       const squares: Array<string> = this.state.squares.slice();
       squares[i] = this.state.xIsNext ? 'X' : 'O';
       this.setState({
           squares: squares,
           xIsNext: !this.state.xIsNext
       });
       console.log(this.state);
   }

   renderSquare(i: number) {
       return <Square
           value={this.state.squares[i]}
           onClick={() => this.handleClick(i)}
       />;
   }

   render() {
       const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

       return (
           <div>
               <div className="status">{status}</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>
       );
   }
}


最後に、勝敗処理判定ロジックを追加します。

# src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

interface SquarePropsInterface {
   value: string;
   onClick: () => void
}

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

interface BoardPropsInterface {
   squares: Array<string>
   xIsNext: boolean
}

interface BoardStateInterface {
   squares: Array<string>
   xIsNext: boolean
   winner: string
}

class Board extends React.Component<BoardPropsInterface, BoardStateInterface> {
   constructor(props: BoardPropsInterface) {
       super(props);
       this.state = {
           squares: Array(9).fill(""),
           xIsNext: true,
           winner: ""
       };
   }

   handleClick(i: number) {
       const winner = calculateWinner(this.state.squares);
       if (winner || this.state.squares[i]) {
           return;
       }

       const squares: Array<string> = this.state.squares.slice();
       squares[i] = this.state.xIsNext ? 'X' : 'O';

       this.setState({
           squares: squares,
           xIsNext: !this.state.xIsNext,
           winner: winner
       });
   }

   renderSquare(i: number) {
       return <Square
           value={this.state.squares[i]}
           onClick={() => this.handleClick(i)}
       />;
   }

   render() {
       let status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

       const winner = calculateWinner(this.state.squares);

       if (winner) {
           status = 'Winner: ' + winner;
       }

       console.log(status);

       return (
           <div>
               <div className="status">{status}</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>
       );
   }
}

class Game extends React.Component {
   render() {
       return (
           <div className="game">
               <div className="game-board">
                   <Board
                       squares={Array(9).fill("")}
                       xIsNext={true}
                   />
               </div>
               <div className="game-info">
                   <div>{/* status */}</div>
                   <ol>{/* TODO */}</ol>
               </div>
           </div>
       );
   }
}

// ========================================

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


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


公式チュートリアルではタイムトラベルに関する項目もありましたが、、propsのバケツリレーに飽きてきたのでこの辺で終わりにします。


次回はReduxを使ってTodoListを実装するチュートリアルをやろうと思います。


この記事が気に入ったらサポートをしてみませんか?