オウルです。
前回の続きです。前回は、ReactチュートリアルをTypescriptでゲーム勝者の判定まで実装しました。今回は、タイムトラベルまで実装して完了です。
language | React v17.0 |
os | WSL Ubuntu 21.04 |
editor | VSCode |
Reactチュートリアル
では、タイムトラベル機能の追加から早速はじめます。
historyの型
チュートリアルで紹介されているhistoryの構造を、素直に型定義します。
1 2 3 4 |
// 着手の履歴を保持する型を定義 type History = { squares: SquareValue[] } |
State のリフトアップ
State のリフトアップ、再びにあるように、BoardコンポーネントにあるstateをトップレベルのGameコンポーネントにリフトアップします。
リフトアップ後のBoardコンポーネント
まず、BoardコンポーネントがsquaresとonClickプロパティをGameコンポーネントから受け取るようにPropsを定義します。
”state”と盤面のクリックイベントハンドラである”handleClick”と勝者、または次プレイヤーの案内を表示する”displayStatus”、”calculateWinner”をGameコンポーネントにリフトアップするため、前回Boardコンポーネントの実装と比較すると、なくなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// Gameコンポーネントから受け取るPropsの型定義 // SquareValueの型定義は、 // 合併型 type SquareValue = 'X' | 'O' | null type BoardProps = { squares: SquareValue[], onClick: (i: number) => void }; // Boardコンポーネント const Board = (props: BoardProps) => { 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コンポーネントでのポイントは、チュートリアルでも丁寧に説明されているkey を選ぶでしょう。ここでは”renderMove”関数にあたります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
const Game = () => { // Boardコンポーネントのstateをリフトアップ + 型をHistoryに変更 const [history, setHistory] = useState([{squares: Array<SquareValue>(9).fill(null)}]); const [xIsNext, setXIsNext] = useState(true); // 新規に、いま何手目の状態を見ているのかを表すstateを追加 const [stepNumber, setStepNumber] = useState(0); // BoardコンポーネントからGameコンポーネントにリフトアップ // また新しい履歴エントリをhistoryに追加するように拡張 const handleClick = (i: number): void => { // チュートリアルにあるように、「時間の巻き戻し」をしてからその時点で新しい着手を起こした場合に、 // そこから見て「将来」にある履歴(もはや正しくなくなったもの)を確実に捨て去るために、stepNumber + 1 const shallow_history = history.slice(0, stepNumber + 1); // 表示している盤面の〇☓状態を取得 const current = shallow_history[shallow_history.length - 1]; const squares = current.squares.slice(); if(calculateWinner(squares) || squares[i]) { return; } squares[i] = (xIsNext) ? 'X' : 'O'; // setState 呼び出しは非同期 // https://ja.reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value setHistory(shallow_history.concat([{squares: squares}])); setXIsNext(!xIsNext); setStepNumber(shallow_history.length); } // BoardコンポーネントからGameコンポーネントにリフトアップ const calculateWinner = (squares: SquareValue[]): SquareValue => { 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; } // BoardコンポーネントからGameコンポーネントにリフトアップ const displayStatus = (): string => { const current = history[history.length - 1]; const winner = calculateWinner(current.squares); if (winner) { return 'Winner: ' + winner; } else { return 'Next player: ' + ((xIsNext) ? 'X' : 'O'); } } const renderMove = () => { return history.map((_, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; // リストとkey: 着手順の連番数字をkeyとする // https://ja.reactjs.org/docs/lists-and-keys.html return ( <li key={move}> <button onClick={() => jumpTo(move)}>{desc}</button> </li> ); }); } const jumpTo = (step: number) => { setStepNumber(step); setXIsNext(step % 2 === 0); } return ( <div className="game"> <div className="game-board"> <Board squares={history[stepNumber].squares} onClick={ (i: number) => handleClick(i)} /> </div> <div className="game-info"> <div>{displayStatus()}</div> <ol>{renderMove()}</ol> </div> </div> ); } ReactDOM.render( <Game />, document.getElementById('root') ); |
Typescriptの重要性
ReactチュートリアルをTypescriptに置き換えているときに感じたTypescriptの重要性。Typescriptを使うことによって、Reactの良さを最大限活かせるのは間違いないと感じたため、Typescriptの本を購入してTypescriptの学習も継続中です。この購入したTypescript本がとても、とても良い。機会があれば紹介したいと思います。
このチュートリアルでは、副作用フックは出てきていないため、次は、Example ProjectsのBMI Calculatorに挑みます。BMI Calculatorは、javascriptのスプレッド構文、Typescriptではルックアップ型、ReactではuseEffectと面白そうです。