React公式のチュートリアルをリファクタする

2021-01-03

Reactの公式チュートリアルは、Reactを最初に学ぶときに実際に手を動かしながらコードを書いて理解することができます。

内容としては、三目並べを実装していきます。チュートリアルを順番にやっていき完成したソースコードのようになり一応アプリケーションとしては完成します。

チュートリアル的にはここまでできれば十分ではあるものの一歩先を考えると、以下の点が気になります。

  • クラスコンポーネントで書かれている。
  • 1つのファイルにViewやロジックが入っており見通しが悪い。(ファイル分割されていない)
  • TypeScriptになっていない。

ということで上記の点をReactのリハビリも兼ねてリファクタリングしてみました。

完成版はこちらのリポジトリにあります。

一応commitを追っていただければ、それっぽく何をしたか確認いただけると思います。

まずはチュートリアル通りに

公式のチュートリアル通りにやっていきますが、この時点でコンポーネントだけは分割しました。

TypeScript対応

create-react-appする際に--template typescriptのオプションを指定していなかったので後追いで必要なパッケージを追加しています。

クラスコンポーネントのままTypeScriptする際は、以下のようにします。

// propsの型を設定する
interface State {
  history: History[]; // 別ファイルで指定した型情報
  stepNumber: number;
  xIsNext: boolean;
}

// React.Componetの後にpropsの型と空オブジェクトを設定
class Game extends React.Component<{}, State> {
  constructor(props: {}) {
    super(props)
    this.state = {
      history: [{
        squares: Array(9).fill(null)
      }],
      stepNumber: 0,
      xIsNext: true
    }
  }

今回は簡単なアプリケーションなので以下の型情報だけ追加しました。

  • SquareTypeは、Union型で、盤面に表示する文字列と空欄の場合にnullにしておく
  • Historyは盤面に表示されている状態を保存しておくためのprops用の型
export type SquareType = 'X' | 'O' | null

export interface History {
  squares: SquareType[]
}

関数コンポーネントに書き換える。

クラスコンポーネントでstateを使用していない箇所は、後述するHooksを使用せずとも関数ベースの記述に変更できます。 例えば、Board.tsxの場合は以下のように変更できます。

※boardNumbersの定数は普通に{Array<number>(9).fill(0).map()みたいな感じで書く方がシンプルかも。

import React from 'react'
import Square from './square'
import { SquareType } from '../../types/interface'

interface BoardProps {
  squares: SquareType[]
  onClick: (i: number) => void
}

const boardNumbers = [0, 1, 2, 3, 4, 5, 6, 7, 8]

const Board: React.FC<BoardProps> = ({squares, onClick}) => {
  return (
    <div className="board-row">
      { boardNumbers.map((boardNumber, i) => {
          return (
            <Square
              key={i}
              value={squares[i]}
              onClick={() => onClick(i)}
              boardNumber={boardNumber} />
            )
        })
      }
    </div>
  )
}

export default Board

変更ポイントとしては、

  • React.FC型にすることでnode_modules/@types/react/index.d.tsの型定義を参照できる。(関数コンポーネントだと明確にわかるようになる)
  • thisを書かなくてよくなる。
  • renderを書かなくてよくなる。

Hooksに置き換える。

stateを管理しているコンポーネントを関数コンポーネントに書き換える際は、Hooksを使う必要があります。

こちらのcommitで確認できます。

import React, { useState } from "react"
import Board from "./board"
import { calculateWinner } from '../../utils/calclateWinner'
import { History } from '../../types/interface'

const Game: React.FC = () => {
  // Hooks
  const [history, setHistory] = useState<History[]>([{ squares: Array(9).fill(null)}])
  const [stepNumber, setStepNumber] = useState<number>(0)
  const [xIsNext, setXIsNext] = useState<boolean>(true)

Hooksには色々なフック関数があるので、詳しくは公式ドキュメントを確認していただければと思いますが、今回はuseStateを使います。

  • useStateをreact本体からimportする。
  • stateで定義していたpropsをHooks用に書き換える。
  • useStateは、変数と変数を更新する関数を返すことで引数の値を更新します。
  • 値を更新したい箇所でsetXXXXをすると値が更新されます。
  • constructorで指定してた箇所は不要になるので丸ごと削除。

関数型に慣れている方であれば、Hooksを使用する方が見通しがよいと感じるかもしれないです。

細かいロジックの分離

今回チュートリアルで作成したロジックたちは/utills/common.tsというファイルにまとめてページ側から分離させました。

calculateWinner以外に以下の関数を追加。

  • hasResultWinnerというゲームの勝者が決まっている時とそうではない時で文言を出し分ける関数
  • immutableSquaresDataという盤面をクリックした後に参照している配列がイミュータブルではないコードになっているので、新しい配列を返すようにする関数。

Recoilで状態管理するようにしてみる。

Recoilは、ContextAPIで辛かった部分をいい感じに実現した状態管理ライブラリです。なんとなく使ってみたさがあったので今回はじめて触ってみました。

まずは、インストールします。

yarn add recoil

インストールしたらRecoilで状態管理させたい範囲を<RecoilRoot>で囲います。

※App.tsxの例

import Game from "./views/components/game"
import {
  RecoilRoot
} from 'recoil'

function App() {
  return (
    <RecoilRoot>
      <div className="App">
        <Game />
      </div>
    </RecoilRoot>
  );
}

export default App

使い方

HooksのuseStateは直接のデータを呼び出して扱っていましたが、Recoilでは、Atomと呼ばれるステートオブジェクトを通じてやり取りしていきます。 今回は以下のようなファイルを作成します。

import { atom } from "recoil";

export const historyItems = atom({
  key: "historyItems",
  default: [{ squares: Array(9).fill(null)}]
});

export const stepNumber = atom({
  key: "step",
  default: 0
});

export const xIsNext = atom({
  key: "xIsNext",
  default: true
});

keyは適当でOKです。defaultにはデフォルト値をいれます。今回の場合ですと、useStateで設定していた値をそのまま入れればOKです。

ここまで来たら後は呼び出すだけです。上記のファイルを使用したいコンポーネント(今回はGame.tsx)でインポートした後以下のようにuseStateしていた部分を書き換えます。

const Game: React.FC = () => {
  // Recoil
  const [history, setHistory] = useRecoilState<History[]>(historyItems)
  const [step, setStepNumber] = useRecoilState<number>(stepNumber)
  const [_xIsNext, setXIsNext] = useRecoilState<boolean>(xIsNext)

基本的にはHooksと同じような使い方になりますが、ただ値を取得したい場合(setXXXが不要)などはuseRecoilValueというフックがあります。

さらに読み取りしないで書き込みだけしたい場合には useSetRecoilStateというフックもありReactの場合、値を読み取らない(setだけを行う)= 値が変化したときも再レンダリングされないのでパフォーマンスを考える時によいです。

今回は、使いませんでしたが、Selectorを使うと、Atomの値を加工して取得することもできます。(Vue.jsのcomputedみたいなノリ)更にこのSelectorは非同期処理にも対応しています。

最後に

ここまでやると大分可読性が高くスッキリしたコードになりました。今回は実施していませんでしたが、CSS in JSでcssを書いてUIをリッチにしてみると尚良さそうだと感じました。

また今回Recoilを試せたのは良かったです。Hooksは触ったことあったのですが、ほぼ同じようなノリで書けるし、個人的にはかなり好きなライブラリでした。機能もそこまで多くないので1日あれば習得可能なので 複雑な状態管理ではない場合は使ってみるのはありだと思いました。

デメリットとして、関数内のコードが若干太り気味になります。同時にやっぱりReduxってスゲーなと思いました。実際にプロダクションへの導入は まだまだ厳しい印象でした。


ソースコードはこちらのリポジトリにあります。

Google Analyticsを使っています。