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を実装するチュートリアルをやろうと思います。
この記事が気に入ったらサポートをしてみませんか?