React公式サイトには入門に最適なチュートリアルが用意されています。
マルバツゲームの作成を通して、Reactとその構文について学ぶことができるようになっています。
実際に手を動かしながら学ぶことができるので、一段階ずつ学べるガイドと合わせてスムーズにReactに入門できるような内容です。
公式チュートリアルは、こちらになります。
ただ、公式チュートリアルには次のようないくつかの問題があります。
- 今ではもうほぼ使われていないクラスコンポーネントがベースになっている
- ファイルを分割していないのでコードが冗長
- TypeScriptを使っていない
なので今回の記事では、クラスベースのコンポーネントから関数ベースのコンポーネントへ、JavaScriptからTypeScriptへ、さらにはファイル分割のリファクタリングをします。
リファクタリングを通して、次のことを学べます。
- ReactでTypeScriptを使うための型定義について
- 関数ベースのコンポーネントの書き方
- 適切なファイル分割の方法
なので、この記事を読み終わる頃にはReactのスキルが1段階上がっていると思います。
Reactチュートリアルを一度もやったことがない場合は、まず一回通しでやってみましょう。
その場合は次の記事を参考にどうぞ。
Reactエンジニアを目指している方へ、
知識0からReactエンジニアになるまでのロードマップをまとめました。
こちらも記事もぜひ参考にどうぞ。
スクールでReactを深く学びたい方へ、
Reactを学べるおすすめのプログラミングスクールもまとめてますので、こちらもどうぞ。
Reactのおすすめ教材が知りたい方へ、
現役のReactエンジニアが>Reactのおすすめ教材をまとめました。
こちらも記事もぜひ参考にどうぞ。
- 文系学部(体育学部)卒だけど、Webエンジニアに転職成功
- 現役のReactエンジニア
- 実務10ヶ月でフリーランスとして独立
- 元々勉強が苦手で、大学の偏差値は50ほど
- 血液型はO型、千葉県出身の神奈川県在住、8月生まれの現在26歳
ReactチュートリアルをTypescriptで書き換えるための環境構築方法
まずは、ローカル環境の構築をしていきます。
公式チュートリアルではブラウザ上でコードを書いていけたのですが、今回はファイル分割をするためローカルの環境構築をする必要があります。
では、手順ごとに説明してきます。
手順1
まずは、Node.jsをインストールしていきます。
https://nodejs.org/ja/
こちらからLTS版をダウンロードしてください。
node -v
と打ってバージョンが表示されれば成功です。
手順2
手順1でReactのアプリを作る準備はできたので、早速作りましょう。
コマンで上で以下を実行すればOKです。
npx create-react-app プロジェクト名 --template typescript
npm startを実行して、この画面が表示されていれば完了です。
手順3
最後に今のコードと公式チュートリアルのコードを差し替えます。
まず、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;
}
次にindex.tsxはこのようになります。
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<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 {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(<Game />, document.getElementById("root"));
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;
}
一旦拡張子をindex.jsに変更して、再度画面をリロードし、マルバツゲームが表示されていれば環境構築は完了です。
ReactチュートリアルをJavaScriptからTypeScriptへ書き換える具体的な手順
では、まずTypeScriptへの書き換えをしていきます。
先ほど変更したindex.jsの拡張子を再びtsxに戻します。
エディタ上にエラーがいくつか出ていると思いますので、それらを1つずつ解決していけばOKです。
Gameコンポーネント
まずは、Gameコンポーネントから変更していきます。
type oneSquareType = "O" | "X" | null;
type historyType = {
squares: Array<oneSquareType>;
}
type gameState = {
history: Array<historyType>;
xIsNext: boolean;
stepNumber: number;
}
これらのtypeを追加しましょう。
TypeScriptではtypeを作成することで個別の型やオブジェクトの型などを簡単に扱えるようになります。
次に、使用している関数の引数にそれぞれ型を追記してやれば完成です。
完成したコードはこのようになります。
type oneSquareType = "O" | "X" | null;
type historyType = {
squares: Array<oneSquareType>;
}
type gameState = {
history: Array<historyType>;
xIsNext: boolean;
stepNumber: number;
}
class Game extends React.Component<{}, gameState> {
constructor(props: {}) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i: number) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step: number) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
Boardコンポーネント
同じ要領でBoardコンポーネントもリファクタリングしていきます。
以下が書き換えたBoardコンポーネントになります。
type boardProps = {
squares: Array<oneSquareType>;
onClick: (i: number) => void;
}
class Board extends React.Component<boardProps, {}> {
renderSquare(i: number) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<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>
);
}
}
Squareコンポーネント
最後にSquareコンポーネントをリファクタリングして終了です。
まずは、関数コンポーネントの型指定で使用するVFCをインポートします。
import {VFC} from "react"
次にProps用のtypeを作成し、コンポーネント自体に型を割り当てればOKです。
type squareProps = {
value: oneSquareType;
onClick: () => void;
}
const Square: VFC<squareProps> = (props) => {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
完成形
最後にcalculateWinnerに型を指定してやれば完成です。
TypeScriptにリファクタリングしたコードの完成形はこちらになります。
import React, {VFC} from 'react';
import ReactDOM from 'react-dom';
import './index.css';
type squareProps = {
value: oneSquareType;
onClick: () => void;
}
const Square: VFC<squareProps> = (props) => {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
type boardProps = {
squares: Array<oneSquareType>;
onClick: (i: number) => void;
}
class Board extends React.Component<boardProps, {}> {
renderSquare(i: number) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<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>
);
}
}
type oneSquareType = "O" | "X" | null;
type historyType = {
squares: Array<oneSquareType>;
}
type gameState = {
history: Array<historyType>;
xIsNext: boolean;
stepNumber: number;
}
class Game extends React.Component<{}, gameState> {
constructor(props: {}) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i: number) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step: number) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// ========================================
ReactDOM.render(<Game />, document.getElementById("root"));
function calculateWinner(squares: Array<oneSquareType>) {
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;
}
Reactチュートリアルをクラスコンポーネントから関数コンポーネントへ書き換える具体的な手順
次に、クラスベースとなっている今のコンポーネントを関数コンポーネントに書き換えていきます。
公式のチュートリアルでSquareコンポーネントをクラスコンポーネントから関数コンポーネントに変更したように、他のコンポーネントも関数コンポーネントに変更していきます。
Boradコンポーネント
VFCを型指定で追記して、thisを消すだけで完成です。
type boardProps = {
squares: Array<oneSquareType>;
onClick: (i: number) => void;
}
const Board: VFC<boardProps> = (props) => {
const renderSquare = (i: number) => {
return (
<Square
value={props.squares[i]}
onClick={() => props.onClick(i)}
/>
);
}
return (
<div>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
);
}
Gameコンポーネント
次にGameコンポーネントを関数コンポーネントに変更していくのですが、少し大変な作業になります。
と言うのも、現状GameコンポーネントはStateを持っています。
関数コンポーネントではStateを宣言することができないため、別の方法で状態を持たせる必要があります。
そして、それはuseStateになります。
使い方は簡単でuseStateをインポートしたら、[変数名、set変数名] = useState(初期値)でOK。
宣言した変数がStateの代わりになり、set変数名の方がsetStateに代わりになります。
なので、リファクタしたコードはこんな感じになります。
type oneSquareType = "O" | "X" | null;
type historyType = {
squares: Array<oneSquareType>;
}
const Game = () => {
const [history, setHistory] = useState<Array<historyType>>([{squares: Array(9).fill(null)}])
const [stepNumber, setStepNumber] = useState<number>(0);
const [xIsNext, setXIsNext] = useState<boolean>(true);
const handleClick = (i: number) => {
const copyedHistory = history.slice(0, stepNumber + 1);
const current = history[copyedHistory.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = xIsNext ? "X" : "O";
setHistory(copyedHistory.concat([
{
squares: squares
}
]));
setStepNumber(copyedHistory.length);
setXIsNext(!xIsNext);
}
const jumpTo = (step: number) => {
setStepNumber(step);
setXIsNext((step % 2) === 0);
}
const current = history[stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
Reactチュートリアルを適切にファイルを分割する方法
最後に、今まのままではファイルの中身が長くて読みづらいコードとなっているので、適切にファイル分割をしていきます。
やることは以下の通りです。
- コンポーネントをそれぞれ別ファイルに移動
- Typeをそれぞれ別ファイルに移動
- その他共通関数などを別ファイルに移動
コンポーネントの分割
まずは、コンポーネントのファイル分割をしていきます。
src配下にcomponentsフォルダを作成し、そこにBoard.tsx、Game.tsx 、Square.tsxを作成します。
Typeの分割
次に、Typeを分割していきます。
ほぼどこの現場もTypeはコンポーネントと同じファイルではなく別ファイルで管理しているからです。
Src配下にtypesフォルダを作成し、boardType.ts、historyType.ts、oneSquareType.ts、squareType.tsを作成します。
その他の分割
最後に残りのコードを分割して終了です。
まずは、calculateWinnerが2箇所のファイルで使用されているので移動して、やりましょう。
Src配下にutilsというフォルダを作成し、calculateWinner.tsを作ってやればOKです。
あとは、今Game関数で使用しているstateやstateを変更している箇所を別ファイルで管理しましょう。
一見難しそうですが、カスタムフックというものを使用すれば簡単にできます。
Src配下にhooksというフォルダを作成し、useGameControl.tsの作成します。
完成形
作成した新しいファイルにそれぞれコードを分割していけば完成です。
完成したコードは以下から見ることができます。
https://github.com/hinoharashinya/react-tutorial-hooks
まとめ 【簡単】TypeScriptでReactチュートリアルを書き換える方法
長くなりすぎてしまいました。
Reactのチュートリアルを終えた方は、この記事にあるように何かリファクタをしたりすると、更に理解が深まるのでおすすめです。
最後まで読んでいただき、ありがとうございました。
僕のブログでは、他にプログラミングに関することなどを発信していますので、気になる方はぜひ見てみてください。
Reactエンジニアになりたい人はこちらからどうぞ
Reactを学べるおすすめのプログラミングスクールを知りたい方はこの記事をどうぞ。
Reactのおすすめ教材を知りたい人はこの記事をどうぞ。
「プログラミングスクールが高くて通えない。。」
といった悩みがこのサービスで解決します。
それは、次世代型のサブスクプログラミングスクールになります。
具体的に、このスクールは以下のことが可能です。
- 講師とのマンツーマンレッスン
- 質の高いかなりボリュームのある教材
- 講師に質問し放題
そして、お値段はたったの1,980円から。
これで高いお金を払わずに、エンジニアになることが可能です。
今なら、全額返金保証もあります。
エンジニアを目指す人も年々増えているで、お早めにどうぞ。
当サイト限定の、初月50%OFFクーポン(HINOSHIN)あり
>>侍テラコヤの評判・口コミ|現役エンジニアが実際に使ってみた感想