React チュートリアルの三目並べに Redux を導入する

# はじめに

React に入門する際に、三目並べを作成する[公式チュートリアル](https://ja.reactjs.org/tutorial/tutorial.html)に取り組む方は多いと思います。
実際に運用されている React プロダクトのほとんどは Redux と何らかのミドルウェアを併用していますが、このチュートリアルでは React 単体についてしか学べません。
当然と言えば当然なのですが、折角 React に入門したのですから、そのままの流れで Redux (と react-redux) も導入したいと考えるのが人情というものです。
という訳で、本記事ではその三目並べに Redux を導入してみます。
[Redux 公式チュートリアル](https://redux.js.org/basics/basic-tutorial)と併せて読んでみてください。

また、続編として [redux-observable とかを導入する記事](https://qiita.com/hey_cube/items/a41e6fa0d94993103864)も書いたので、興味があればそちらもどうぞ。

# 事前準備

まず、React 公式チュートリアルの[タイムトラベルの実装](https://ja.reactjs.org/tutorial/tutorial.html#implementing-time-travel)まで済ませましょう。
三目並べが完成すると、 `Game` component がいくつかの状態を持つと思います。
この状態を Redux に管理してもらいましょう。

Redux 導入をやりやすくするために、まずはファイルを分割します。
[ここ](https://github.com/hey-cube/react-tutorial-with-redux/commit/6fc935cd2292238efce3ba2bf4b669d886c32a50)を参考に、以下のファイルに JavaScript コードを分割してください。

- src/components.jsx
- src/index.jsx

# Redux / react-redux 導入

## Redux とは

> Redux is a predictable state container for JavaScript apps.
> 
> It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
> 
> You can use Redux together with React, or with any other view library. It is tiny (2kB, including dependencies), but has a large ecosystem of addons available.

[Getting Started with Redux･Redux](https://redux.js.org/introduction/getting-started) より引用

## react-redux とは

> React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.

[Quick Start･React Redux](https://react-redux.js.org/introduction/quick-start) より引用

## redux / react-redux をインストール

以下のコマンドで redux, react-redux をプロジェクトに追加します。
`yarn` を使用する場合は適宜読み替えてください。

```shell
npm install redux react-redux
```

## action を追加

> Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

[Actions・Redux](https://redux.js.org/basics/actions) より引用
（`store` の説明は後で出てくるので、今はまだわからなくても大丈夫です。）

三目並べで発生する action は以下の 2 つとします。

- どこかのマスがクリックされる
- いずれかの履歴がクリックされる

前者を表現した `action` を以下に示します。

```src/actions.js
/*
 * action types
 */

export const CLICK_SQUARE = "CLICK_SQUARE";

 /*
  * action creators
  */

export function clickSquare(index) {
  return { type: CLICK_SQUARE, index };
}
```

「マスがクリックされた」という情報を表す `CLICK_SQUARE` action type と、「ある場所のマスがクリックされた」という action を生成する action creator が定義できました。

それでは、ここに「いずれかの履歴がクリックされた」を表す `action type` と `action creator` を追加しましょう。
実装例は[ここ](https://github.com/hey-cube/react-tutorial-with-redux/commit/09223f93787a32cb0b5239dd79cca6aecd8aba36)にあります。

## reducer を追加

> Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.

[Reducers・Redux](https://redux.js.org/basics/reducers) より引用

`reducer` の実装を以下に示します。

```src/reducers.js
import { combineReducers } from "redux";
import { CLICK_SQUARE, JUMP_TO_PAST } from "./actions";

 const initialState = {
  history: [
    {
      squares: Array(9).fill(null)
    }
  ],
  stepNumber: 0,
  xIsNext: true
};

 function game(state = initialState, action) {
  switch (action.type) {
    case CLICK_SQUARE:
      const history = state.history.slice(0, state.stepNumber + 1);
      const current = history[history.length - 1];
      const squares = current.squares.slice();
      if (calculateWinner(squares) || squares[action.index]) {
        return state;
      }
      squares[action.index] = state.xIsNext ? "X" : "O";
      return {
        history: history.concat([
          {
            squares: squares
          }
        ]),
        stepNumber: history.length,
        xIsNext: !state.xIsNext
      };

     default:
      return state;
  }
}

 export const app = combineReducers({ game });

 // ========================================

 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;
}
```

`initialState` は Redux の `state` の初期値です。
ちなみにこれは `Game` component の state をそのまま持ってきただけです。

`game()` が `reducer` です。
こちらも `Game` component の `handleClick()` とほとんど同じです。

`combineReducers()` は複数の `reducer` を 1 つにまとめるための関数です。
（今回の例では大した役割を担っていないので、あまり気にしなくて良いです。）

それでは、ここに「履歴がクリックされた」という action に対応する処理を追加しましょう。
実装例は[ここ](https://github.com/hey-cube/react-tutorial-with-redux/commit/be72914aacb56e2c61dcd42c89f5d9cee7d15e1a)にあります。

## store を追加

> In the previous sections, we defined the actions that represent the facts about “what happened” and the reducers that update the state according to those actions.
> 
> The Store is the object that brings them together. 

[Store・Redux](https://redux.js.org/basics/store) より引用
（ここはとても重要なところなので、Redux をよく知らない人は引用元をよく読むことをお勧めします。）

`createStore()` 関数に `reducer` を渡すことで `store` を作ることができます。

```src/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Game } from "./components";
import { app } from "./reducers";
import "./index.css";

const store = createStore(app);
ReactDOM.render(<Game />, document.getElementById("root"));
```

ここで作成した `store` は、 `container component` を追加した後に使用します。

## container component を追加

> 私がContainerと名付けたコンポーネントの特徴は以下の通りです。
> 
> - どのように機能するか、ということと結びついてる。
> - PresentatinalコンポーネントとContainerコンポーネントの両方を内部に持つことができるが、たいていの場合は自分自身のためのDOMマークアップとスタイルを「持たない」。
> - データと挙動を、Presentationalコンポーネントや、他のContainerコンポーネントに対して提供する。
> - Fluxのアクションをcallしたり、アクションをコールバックとしてPresentatinalコンポーネンへ提供する。
> - たいていの場合は状態を持ち、データの源としての役割を担う。
> - higher order componentを用いることで生成される。例えばReact Reduxのconnect()やRelayのcreateContainer()やFlux UtilsのContainerCreate()である。

[日本語訳: Presentational and Container Components](http://better-than-i-was-yesterday.com/presentational-and-container-components/) より引用
（[Usage with React・Redux](https://redux.js.org/basics/usage-with-react#presentational-and-container-components) にも記述があるので、そちらも参照してみてください。）

React / Redux を使う時の `container component` というと、だいたいこんな感じになります。

```src/containers.js
import { connect } from "react-redux";
import { clickSquare, jumpToPast } from "./actions";
import { Game } from "./components";

 const mapStateToProps = (state, ownProps) => {
  return state.game;
};

 const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    handleClick: index => {
      dispatch(clickSquare(index));
    },
    jumpTo: () => {}
  };
};

 export const GameContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(Game);
```

`mapStateToProps()` は、Redux の `state` を props として適当な形に整形する関数です。

`mapDispatchToProps()` は、Redux の `dispatcher` を props として適当な形に整形する関数です。
（`action` を `store` に渡すことを `dispatch` と呼び、それをする関数のことを `dispatcher` と呼びます。）

`connect()()` は、Redux の `state` と React component を接続する関数です。
`mapStateToProps` ないし `mapDispatchToProps` と React component を渡すと、container component が返ってきます。

これで `Game`  component は `GameContainer` component から Redux の諸々を props 経由で受け取ることができるようになりました。
なので、それらを使うよう書き換えましょう。

```src/components.jsx
export class Game extends React.Component {
  render() {
    const history = this.props.history;
    const current = history[this.props.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? `Go to move #` + move : "Go to game start";
      return (
        <li key={move}>
          <button onClick={() => this.props.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.props.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={i => this.props.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}
```

これでこの component は内部に state を持たないし、外部の state （つまり Redux の `state` ）の存在も知らないものになりました。
アプリケーションの状態と完全に切り離すことができたので、この component はテストをしやすいはずです。

さて、先ほど提示した `mapDispatchToProps()` が返却している `jumpTo` プロパティには不足があります。
これを完成させましょう。
実装例は[ここ](https://github.com/hey-cube/react-tutorial-with-redux/commit/75049b65449c510278956fcba9bc4681ec765d23)にあります。

## container component と Redux の state を接続

それでは、今まで `Game` component を呼び出していた箇所を `GameContainer` に書き換えましょう。

```src/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { Game } from "./components";
import { app } from "./reducers";
import "./index.css";

const store = createStore(app);
ReactDOM.render(<GameContainer />, document.getElementById("root"));
```

次に、 `Provider` component を導入します。

> The option we recommend is to use a special React Redux component called \<Provider\> to magically make the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component:

[Usage with React・Redux](https://redux.js.org/basics/usage-with-react#passing-the-store) より引用

```src/index.jsx
import React from "react";
import { render } from "react-dom";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { app } from "./reducers";
import { GameContainer } from "./containers";
import "./index.css";

const store = createStore(app);

render(
  <Provider store={store}>
    <GameContainer />
  </Provider>,
  document.getElementById("root")
);
```

`Provider` component は配下にいる container component に `redux state` と `dispatcher` を良い感じに渡してくれます。
これで三目並べに Redux を導入できたはずです。

ここに、以下のような変更を加えても良いでしょう。

- `calculateWinner()` を `src/utils.js` に切り出す
- 全ての React component を functional component にする
- `Game` component の `render()` 内で定義されている `current` および `status` を `mapStateToProps()` 内に移す
- `Board` component に渡されている `onClick()` および `squares` props を Redux の `state` から直接受け取るようにする

これで Redux の導入が完了しました。
ちなみに、導入の全容は[このリポジトリ](https://github.com/hey-cube/react-tutorial-with-redux)にあります。

# 更なる発展

プロダクトとして使うには（場合によりますが）まだ不十分です。
例えば以下のようなものが必要でしょう。

- ミドルウェア
    - [redux-saga](https://github.com/redux-saga/redux-saga/blob/master/README_ja.md) など
    - 三目並べに [redux-observable](https://github.com/redux-observable/redux-observable) を導入する記事を ~~後日書きます~~ 書きました↓
        - [React チュートリアルの三目並べに redux-observable とかを導入する](https://qiita.com/hey_cube/items/a41e6fa0d94993103864)
- TypeScript
    - React + Redux なフロントを構築するのであれば、[TypeScript](http://www.typescriptlang.org/) は必須です
- テスト機構
      - [jest](https://jestjs.io/ja/) や [enzyme](https://github.com/airbnb/enzyme) など
- 拡張性のあるディレクトリ構造
    - [Re-ducks パターン](https://noah.plus/blog/021/)など
- router ライブラリ
    - [@reach/router](https://reacttraining.com/blog/reach-react-router-future/) など
- CSS ライブラリ
    - 僕の最近のお気に入りは [Spectre.css](https://picturepan2.github.io/spectre/) です
- エラー監視ツール
    - [Sentry](https://sentry.io/welcome/) など
- サイト分析ツール
    - [Google Analytics](https://analytics.google.com/analytics/web/provision/?authuser=0#/provision) や [Intercom](https://www.intercom.com/) など