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の基本ともいうべきコンポーネントという概念について。
これ、マジで気に入ってます。単体テストが楽になるんじゃないでしょうか。
オブジェクト指向とかマイクロサービスとか、そういうカタチのフロント版ってイメージ。
コンポーネントの作り方にセンスが問われそう?
実務で携わるのが楽しみです。
以上です。
コメントを残す