React Native for Web + TypeScriptを使ってReact公式のチュートリアルをやってみた
React公式ページのチュートリアルをReact Native for WebとTypeScriptを使ってやってみました。
実装するもの
- React公式ページのチュートリアルの三目並べゲーム
今回作成したソースコードの一式はこちらにあります。
実装方針
- Create React Appを使ってプロジェクトを作成する
- TypeScriptを使用する
- 画面の作成に、React Native for Webのコンポーネントを使用する
- チュートリアルの内容(*2020/3/2時点)に沿う
事前準備
Create React App
を使ってReactアプリを作成し、TypeScriptとReact Native for Webを導入します。今回は以下の記事でご紹介した方法でReactアプリを作成し、TypeScript
、React Native for Web
、Prettier
を導入しました。
スターターコードを実装する
スターターコードにある3つのコンポーネントSquare(正方形のマス目)
、Board(盤面)
、Game
をReact Native for Webを使って作成します。
srcディレクトリ内にcomponentsディレクトリ
を作成し、その中に各コンポーネントを配置しました。
Squareコンポーネント(正方形のマス目)の実装
まず、三目並べゲームのマス目となるSquareコンポーネントを作成していきます。
src/components/Square.tsx
import React from 'react' import { StyleSheet, TouchableOpacity, Text } from 'react-native' const styles = StyleSheet.create({ container: { width: 80, height: 80, borderWidth: 1, borderColor: 'black', justifyContent: 'center', alignItems: 'center' }, text: { fontSize: 24, } }) function Square() { return ( <TouchableOpacity style={styles.container}> <Text style={styles.text}>x</Text> </TouchableOpacity>) } export default Square
Squareコンポーネントは、React Native for WebのStyleSheetコンポーネント
、Touchable Opacityコンポーネント
、Textコンポーネント
を使って作成しました。
Touchable Opacityコンポーネントを使うと、クリック操作時に透明度が下がり、クリックされたことが可視化できるようになります。Textコンポーネントは文字通りテキストを表示するためのコンポーネントで、今はx
を指定しています。
StyleSheetコンポーネントは各コンポーネントのレイアウトを設定するために必要となるコンポーネントです。React Native / React Native for Webは、レイアウトの設定にcssファイルではなく、StyleSheetコンポーネントを使用します。レイアウトの設定についての詳細は、React Nativeの公式ドキュメントを参照してください。
ここで、Squareコンポーネントがどう表示されるか確認してみましょう。
index.tsx
を以下のように変更してyarn start
を実行すると、正方形のマス目が1つ表示されるのが確認できると思います。
src/index.tsx
import React from 'react' import ReactDOM from 'react-dom' import Square from './components/Square' ReactDOM.render(<Square />, document.getElementById('root'))
ブラウザの表示
Boardコンポーネント(盤面)の実装
続いて、盤面となるBoardコンポーネントを作成します。
src/components/Board.tsx
import React from 'react' import { StyleSheet, View, Text } from 'react-native' import Square from './Square' const styles = StyleSheet.create({ container: { alignItems: 'center', marginVertical: 10, }, status: { marginBottom: 10 }, rowContainer: { flex: 1, flexDirection: 'row', } }) function Board() { const status = 'Next player: X' const renderSquare = (value: number) => { return <Square /> } return ( <View style={styles.container}> <Text style={styles.status}>{status}</Text> <View style={styles.rowContainer}> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </View> <View style={styles.rowContainer}> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </View> <View style={styles.rowContainer}> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </View> </View> ) } export default Board
Boardコンポーネントは先程作成したSquareコンポーネントを使って作成します。
React Native for WebのViewコンポーネント
を使ってSquareコンポーネント x 3枚のブロックを作り、rowContainer
にflexDirection: 'row'を設定して、横に並べています。
index.tsx
の<Square />
を<Board />
に変更してyarn start
を実行すると、9マスの盤面が表示されるようになりました。
ブラウザの表示
Gameコンポーネントの実装
続いて、Boardコンポーネントを配置するためのコンポーネントとして、Gameコンポーネントを作成します。またindex.tsをGameコンポーネント
を呼び出すように修正します。
src/components/Game.tsx
import React from 'react' import Board from './Board' function Game() { return <Board /> } export default Game
src/index.tsx
import React from 'react' import ReactDOM from 'react-dom' import Game from './components/Game' ReactDOM.render(<Game />, document.getElementById('root'))
ここまでで三目並べゲームで使用するコンポーネントの準備ができました。続いて動作するゲームにするための実装をしていきます。
Boardコンポーネント(盤面)に X と O を交互に置けるようにする
Squareコンポーネント(マス目)に、Boardコンポーネント(盤面)から渡した値を表示できるようにする
まず、Squareコンポーネントに、X or 0 を表示できるように修正します。また、マス目の値はBoardコンポーネントから渡せるように型Props
を定義します。マス目の初期値は何も置かれていないため、null
の状態を保持できるよう、value値の型はTypeScriptの共用型を使ってvalue: 'X' | '0' | null
と定義します。
src/components/Square.tsx
+ type Props = { + value: 'X' | '0' | null + } - function Square() { + function Square(props: Props) { + const { value } = props return ( <TouchableOpacity style={styles.container}> - <Text style={styles.text}>x</Text> + <Text style={styles.text}>{value}</Text> </TouchableOpacity> ) }
続いてBoardコンポーネントに、全てのマス目の状態を保持させ、各Squareコンポーネントに渡すようにします。
ステートフックを使用して、全てのマス目の状態を配列のデータsquares
に保持するようにします。また、全てのマス目の初期値は何も置かれていないことを示すnull
をセットします。
src/components/Board.tsx
function Board() { const status = 'Next player: X' + const initialSquares: Array<'X' | '0' | null> = Array(9).fill(null) + const [squares, setSquares] = React.useState(initialSquares) - const renderSquare = (value: number) => { - return <Square /> - } + const renderSquare = (i: number) => { + return <Square value={squares[i]} /> + } // ...略 }
これで、BoardコンポーネントからSquareコンポーネントにX or 0 or null
の値を渡すことができるようになりました。
Squareコンポーネント(マス目)のクリック時の動作を、Boardコンポーネント(盤面)から渡せるようする
まず、SquareコンポーネントのPropsにonPress
を定義し、TouchableOpacityコンポーネントのonPressプロパティ
にセットします。これで、クリック時の動作を他のコンポーネントから受け取れるようになりました。
src/components/Square.tsx
type Props = { value: 'X' | '0' | null + onPress: () => void } function Square(props: Props) { const { value, onPress } = props return ( - <TouchableOpacity style={styles.container} > + <TouchableOpacity style={styles.container} onPress={onPress}> <Text style={styles.text}>{value}</Text> </TouchableOpacity> ) }
続いて、Squareコンポーネントがクリックされた時の動作をBoardコンポーネントから渡していきます。handlePress関数
を定義し、クリックされたマス目の状態をX
に変更するように実装しました。
src/components/Board.tsx
function Board() { // ...略 const [squares, setSquares] = React.useState(initialSquares) + const handlePress = (i: number) => { + const newSquares = squares.slice() + newSquares[i] = 'X' + setSquares(newSquares) + } const renderSquare = (i: number) => { - return <Square value={squares[i]} /> + return <Square value={squares[i]} onPress={() => handlePress(i)} /> } // ...略 }
SquareコンポーネントをクリックするとX
が表示されるようになりました。
ブラウザの表示
先手はX
、後手は0
が交互に表示されるようにする
Boardコンポーネントに次が自分の手番かどうかの情報を保持するように変更します。この変更によってBoardコンポーネントには9マスそれぞれの情報
と次が自分の手番かどうかの情報
を保持することとなったため、その情報をSquaresとして関数の外部に型定義することにしました。
type Squares = { values: Array<'X' | '0' | null>, xIsNext: boolean }
values
は各マス目の情報(x/0/null)を配列で保持し、xIsNext
は次が自分の手番かどうか(true/false)の情報を保持します。
そして、status
テキストには、xIsNext
の値に応じて、どちらのプレーヤの手番なのかを表示するようにし、Squareコンポーネントのタップ時にxIsNext
の値を反転させるように修正を加えました。
src/components/Board.tsx
function Board() { - const status = 'Next player: X' - const initialSquares: Array<string | null> = Array(9).fill(null) + const initialSquares: Squares = { + values: Array(9).fill(null), + xIsNext: true + } const [squares, setSquares] = React.useState(initialSquares) + const status = 'Next player: ' + (squares.xIsNext ? 'X' : '0') const handlePress = (i: number) => { - const newSquares = squares.slice() - newSquares[i] = 'X' - setSquares(newSquares) + const values = squares.values.slice() + values[i] = squares.xIsNext ? 'X' : '0' + setSquares({ + values: values, + xIsNext: !squares.xIsNext + }) } const renderSquare = (i: number) => { - return <Square value={squares[i]} onPress={() => handlePress(i)} /> + return <Square value={squares.values[i]} onPress={() => handlePress(i)} /> } // ...略 }
これで、盤面に X と O を交互に置けるようになりました。
ブラウザの表示
ゲーム勝者の判定を加える
ゲームが決着して次の手番がなくなった時に勝者を表示する
チュートリアルの内容の通り、BoardコンポーネントにcalculateWinner関数
を追加し、その結果に応じて勝者を表示します。
src/components/Board.tsx
+ function calculateWinner(squares: 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 [one, two, three] = lines[i] + if (squares.values[one] && squares.values[one] === squares.values[two] && squares.values[one] === squares.values[three]) { + return squares.values[one] + } + } + return null + } function Board() { const initialSquares: Squares = { values: Array(9).fill(null), xIsNext: true } const [squares, setSquares] = React.useState(initialSquares) - const status = 'Next player: ' + (squares.xIsNext ? 'X' : '0') + const winner = calculateWinner(squares) + let status: string + if (winner) { + status = 'Winner: ' + winner + } else { + status = 'Next player: ' + (squares.xIsNext ? 'X' : '0') + } // ...略 }
ゲームの決着が既についている場合や、既に埋まっているマス目をクリックされた場合に、マス目を更新しないようにする
こちらもチュートリアルの内容の通りに処理を追加しました。
src/components/Board.tsx
function Board() { // ...略 const handlePress = (i: number) => { const values = squares.values.slice() + if (calculateWinner(squares) || squares.values[i]) { + return + } values[i] = squares.xIsNext ? 'X' : '0' setSquares({ values: values, xIsNext: !squares.xIsNext }) } // ...略 }
以上で、ゲーム勝者の判定を加えることができました
ブラウザの表示
リセットボタンを追加する
チュートリアルではこの後、時間を巻き戻す
機能の実装がされていますが、ここでは勝負の途中、勝敗がついた後にリセットできるようリセットボタンを追加してみます。
リセットボタンの実装にはButtonコンポーネント
を使用しました。そしてリセット時の処理をresetSquares
に実装し、ButtonコンポーネントのonPressプロパティ
に渡します。
src/components/Board.tsx
import React from 'react' - import { StyleSheet, View, Text} from 'react-native' + import { StyleSheet, View, Text, Button } from 'react-native' import Square from './Square' const styles = StyleSheet.create({ // ...略 + reset: { + marginVertical: 10 + } }) // ...略 function Board() { // ...略 + const resetSquares = React.useCallback( + () => setSquares(initialSquares), [setSquares, initialSquares] + ) return ( <View style={styles.container}> // ...略 + <View style={styles.reset}> + <Button title="リセットする" onPress={resetSquares} /> + </View> </View> ) }
以上で、三目並べゲームの完成です。
ブラウザの画面
まとめ
React公式のチュートリアルをReact Native for Web + TypeScriptで実装した内容をご紹介しました。React Native for Web
を使用したことで、画面を効率よく作成することができました。
この記事がどなたかのお役に立てば幸いです。