はじめに
都内某所で(一応)フルスタックエンジニアとして働いているRyuと申します。
エンジニア歴としては4年目になりますが、まだまだ何もかも足りない雑魚エンジニアです。涙
しかし、雑魚エンジニアのままではいけない!と思い立ち、自身のスキルアップやアウトプット習慣づけのためにQiitaに記事を投稿してみることにしました。
今回は僕が業務で扱っている技術であるReactに焦点を当て、記事を書いていこうと思います。
本記事の目的
皆さんは新しい技術に触れる時、公式ドキュメントは読んでいますでしょうか?
英語読めないし、日本語の技術記事呼んだほうが早い…などと思い、僕はあまり読んでいませんでした。
そんな自身を戒めるべくReactの公式チュートリアルを読み進めていると、Tic-Tac-Toe(日本語だと三目並べと呼ぶらしい。いわゆる〇×ゲーム)作成する章の終わりにこんな一文がありました。
要約すると「暇な場合、もしくはもっとReactのスキルを磨きたい人はこれらの機能も実装してみてね」とのことです。
そう言われたら実装するしかない!ので実装してみることにします。
導入
Reactの開発を進めるためにまずはcreate-react-app
でローカル環境を構築しましょう。
Reactの環境構築についてはこの記事では省略します。
create-react-app
して出来たディレクトリに移動してnpm start
するとhttp://localhost:3000/
でReactの初期画面が表示されるのではないかと思います。
この記事では上記で作成したプロジェクト内の以下のファイルをいじっていきます。
・App.js
… 三目並べの機能を記述
・App.css
… 三目並べのスタイルを記述
・index.css
… 全体に適応させたいスタイルを記述
ではまずは公式チュートリアルですでに記載されているソースコードをコピペして動かしてみましょう。
App.js
の中身を公式チュートリアルにあるコードで書き換えます。
このコードをブラウザで確認すると、、、、
機能はちゃんとうごいているっぽい、、、!
ただこのままでは見づらいのでスタイルを整えてあげましょう。
以下ソースコードです。
import { useState } from 'react';
import './App.css';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-table">
<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>
</div>
</>
);
}
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li className="game-info-item" key={move}>
<button className="game-info-btn"onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol className="game-info-list">{moves}</ol>
</div>
</div>
);
}
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;
}
.game {
margin: 50px auto;
}
.status {
margin-bottom: 30px;
font-size: 24px;
font-weight: bold;
}
.board-table {
border-collapse: collapse;
margin: 0 auto;
}
.board-row {
display: flex;
}
.square {
width: 100px;
height: 100px;
border: 2px solid #000;
font-size: 24px;
cursor: pointer;
background-color: #ffffff;
}
.square:hover {
background-color: #c8e7fa;
}
.game-info-list {
padding: 0;
list-style-type: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.game-info-item {
width: 100%;
text-align: center;
}
.game-info-btn {
background-color: #9cd9ff;
padding: 15px 30px;
border: none;
border-radius: 5px;
font-size: 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.game-info-btn:hover {
background-color: #69c6ff;
}
html{
margin: 0;
display: flex;
justify-content: center;
background-color: #ffffff;
overflow-y: scroll;
}
ある程度見やすくなりましたね!
この状態をスタートとして追加機能の実装に入っていきます。
(ここまでの機能の内容は公式チュートリアルに詳細が載っているので気になる方はそちらを参考にしてください<(_ _)>)
現在のターンを示せ!
冒頭で取りあげた公式からの挑戦状には全部で5つの機能が挙げられていました。
まずは一つ目の機能から追加していきたいと思います。
それがどんな機能なのかというと、
1. For the current move only, show “You are at move #…” instead of a button.
Google翻訳すると、「現在の移動の場合のみ、ボタンの代わりに「移動中 #… です」と表示する。」
...若干意図がつかみにくいですが、現状Go to move #1,2,3...となっているボタンのうち、現在のターンに該当するボタンが分かるようになればいいと解釈しました。
エンジニアたるもの要件の本質さえ押さえていれば、仕様を柔軟に変更することも大切...なはず!(持論)
というわけで現在ボードに表示しているターンに該当するボタンの色をグレーにしてみましょう。
実装
ではまず実装するにはどうしたらいいか考えてみます。
「現在ボードに表示しているターンに該当するボタンの色をグレーにする」には、「現在のターン」と「ボタンに対応しているターン」が一致した場合、その該当するボタンに色を変更する用のクラスを当ててあげればよさそうです。
そのため、ソースコードでボタンを生成している箇所を見てみます。
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li className="game-info-item" key={move}>
<button className="game-info-btn" onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
このmoves
は最終的にゲームの履歴を司るボタンの要素を返却しています。
ここで返却する際に色変える用のクラスを当ててあげればよさそうです。
ここのコードについて詳細を確認してきましょう。
最初にmapしているhistory
はゲームの履歴を記憶しています。
そしてmapしているのでmove
がターン数に相当します。(0が初期状態で、盤面がすべて埋まった状態だと9)
あとは現在のターンさえ分かればうまいこと該当するボタンにクラスを付与できそうです。
では現在のターンを記憶しているステートなどないかなとソースを探してみると、currentMove
というものがありました。
const [currentMove, setCurrentMove] = useState(0);
チュートリアル内の説明によると、どうやらこちらのステートで現在のターン数を記憶しているようです。
ではmove
とcurrentMove
を比較して一致したら色変更用クラスを付与する記述を加えてみましょう。
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
// 現在のターンに該当するボタンに 'current-move' クラスを追加
let buttonClass;
if (move === currentMove) {
buttonClass = 'game-info-btn current-move'
} else {
buttonClass = 'game-info-btn';
}
return (
<li className="game-info-item" key={move}>
<button className={buttonClass} onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
.game-info-btn.current-move {
background-color: #d7d7d7;
}
ちゃんと現在のターンを表すボタンの色がグレーになっていますね!!
これで「現在ボードに表示しているターンに該当するボタンの色をグレーにする」機能は実装できたといっていいでしょう。
記事が長くなってしまうので他の追加機能については記事を分けて投稿しようかと思います。
もし何か間違ってるよ~とか、ここはこうした方がもっと効率よく書けるよ~などありましたら気軽にコメントいただけると幸いですm(__)m
次の記事↓
React公式チュートリアルの三目並べに追加機能を実装してみた #2(ループ処理でボードを表示しろ!編)
他の関連記事↓
React公式チュートリアルの三目並べに追加機能を実装してみた #3(トグルボタンを追加しろ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #4(勝利のマスを強調しろ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #5(各ターンで打った手を記憶しろ!編)
React公式チュートリアルの三目並べに追加機能を実装してみた #6(AWSで公開しろ!編)