ちょっと話題の記事

React Native for Web + TypeScriptを使ってReact公式のチュートリアルをやってみた

2020.03.02

React公式ページのチュートリアルをReact Native for WebTypeScriptを使ってやってみました。

実装するもの

今回作成したソースコードの一式はこちらにあります。

実装方針

  • Create React Appを使ってプロジェクトを作成する
  • TypeScriptを使用する
  • 画面の作成に、React Native for Webのコンポーネントを使用する
  • チュートリアルの内容(*2020/3/2時点)に沿う

事前準備

Create React Appを使ってReactアプリを作成し、TypeScriptReact Native for Webを導入します。今回は以下の記事でご紹介した方法でReactアプリを作成し、TypeScriptReact Native for WebPrettierを導入しました。

スターターコードを実装する

スターターコードにある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枚のブロックを作り、rowContainerflexDirection: '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を使用したことで、画面を効率よく作成することができました。

この記事がどなたかのお役に立てば幸いです。