20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-21

はじめに

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

また、続編として redux-observable とかを導入する記事も書いたので、興味があればそちらもどうぞ。

事前準備

まず、React 公式チュートリアルのタイムトラベルの実装まで済ませましょう。
三目並べが完成すると、 Game component がいくつかの状態を持つと思います。
この状態を Redux に管理してもらいましょう。

Redux 導入をやりやすくするために、まずはファイルを分割します。
ここを参考に、以下のファイルに 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 より引用

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 より引用

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

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

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 より引用
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 typeaction creator を追加しましょう。
実装例はここにあります。

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 より引用

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 に対応する処理を追加しましょう。
実装例はここにあります。

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 より引用
(ここはとても重要なところなので、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 より引用
Usage with React・Redux にも記述があるので、そちらも参照してみてください。)

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 として適当な形に整形する関数です。
actionstore に渡すことを 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 プロパティには不足があります。
これを完成させましょう。
実装例はここにあります。

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 より引用

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 statedispatcher を良い感じに渡してくれます。
これで三目並べに Redux を導入できたはずです。

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

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

これで Redux の導入が完了しました。
ちなみに、導入の全容はこのリポジトリにあります。

更なる発展

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

20
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?