trifle

技術メモ

A Complete React Redux Tutorial for 2019 を読んで Redux に入門する

これまで名前しか聞いたことがなかった Redux をプライベートで知る必要が出てきたかもしれないので, 入門してみることにしました. ちょうど最近,

daveceddia.com

という記事が出たようなので, これを手がかりに, 自分用のメモを残してみます.



Redux を使うと何がうれしいのか

React では, ある親 Component から子 Component に状態を受け継ぐ時, 子 Component はそれを props として引き受けます.
例えば以下のように記述された 3 * 3 のマス目のようなものを考えてみてください.

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

function Board(props) {
  const renderSquare = i => (
    <Square value={i} onClick={() => props.onClick(i)} />
  );

  return (
    <div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
    </div>
  );
}

function Game() {
  const handleClick = i => {
    console.log(i);
  };

  return (
    <div className="game">
      <div className="game-board">
        <Board onClick={i => handleClick(i)} />
      </div>
    </div>
  );
}

Square を定義する関数の中に props.value というのがあり, これはひと階層上の Board で示された value={i} を受け取っているということです.
また, Square の要素をクリックすると, props.onClick に伝わりますが, これはやはり同様に Board で示された onClick={() => props.onClick(i)} を呼び出します. そしてそれはさらに上の階層の GameonClick={i => handleClick(i)} を呼び出します.
このようにデータの受け渡しが Component の親子関係で行われます.
以上のような単純な例であれば問題無いですが, このデータの受け渡しの親子関係が, 曽祖父からやしゃごまで, と言えるくらい深い階層間で行われるとすると大変ですね. それに, その深い階層間の途中にある Component にとって, その受け渡されていくデータが無関係なものだとしたら, 無意味にそれらの Component の情報を増やすことになるので, ソフトウェアの設計上あまり良くないと言えるでしょう.


Redux はデータを一元的に管理することによってこの問題の解決を図ります.

2019 年現在なら Context API でもいいかも?

私自身は React 初心者なので, 経験に基づいた知見ではないことをご了承ください

Redux は強力なゆえに, 小規模な開発ではオーバーキルになってしまう可能性があります.
単に直接孫にデータを渡すだけであれば React ビルトインの Context API を使うのでも良さそうです.

Redux の導入

npm install redux react-redux

redux が本体, react-redux がそれを React と繋げるもの, と考えればよいでしょう.

Store

Redux の概念です. あらゆる state を管理するただ一つの倉庫です.

Reducer

Redux の概念です. ただの関数ですが, Store に保管された state を更新するという役目を担っています.
(ES6の Array にもありますが)畳み込み演算について知識があれば reduce という名前は聞いたことがあるでしょう.

[1, 2, 3, 4, 5].reduce((i, j) => i * j)  // 120

上記の計算を大仰に解説すると, 初期状態 1 に対して, 受け渡された 2 を掛け合わせて 2 に, 受け渡された 3 を掛け合わせて 6 に, 受け渡された 4 を掛け合わせて 24 に, 受け渡された 5 を掛け合わせて 120 になります. ここで重要なのは, 状態に対して, 受け渡された値を掛け合わせるという関数を適用し, 新たな状態を作ることをひたすら繰り返していることです.
Redux の Reducer にも同じことが言えます. つまり Reducer に状態を渡すことで, 新たな状態が生まれるというわけです.

Reducer について留意すべき点は

  • undefined を返さないようにする
  • 副作用を持たせないようにする

ことです.

Action と Dispatch

Redux の概念です.
まず, Action ですが, 実体としてはただのオブジェクトで, Reducer の中で行われる処理の識別子として考えればよいでしょう.
Action 単体では何も起こりません. これは Dispatch と合わせて使うものです. dispatch() という関数の引数に Action を指定してやることで, その Action にもとづいて store が操作されます.

Redux の例

ここまでの内容を整理するために, 具体的なコードの例を使いましょう.
簡単なカウンターを考えます. 初期値は0で, 1増やす操作や1減らす操作, リセットする操作などができればよさそうです.

import { createStore } from 'redux';

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        count: state.count - 1
      };
    case 'RESET':
      return {
        count: 0
      };
    default:
      return state;
  }
}

これが Reducer の一例です. これをもとに

const store = createStore(reducer)

このように Reducer を引数にとって Store を定義できます.
実際にこの Store に対して Action を Dispatch するには

store.dispatch({ type: 'INCREMENT' }); // count を 1 に
store.dispatch({ type: 'INCREMENT' }); // count を 2 に
store.dispatch({ type: 'DECREMENT' }); // count を 1 に
store.dispatch({ type: 'RESET' }); // count を 0 に

このように記述してやればよいです.
{ type: 'INCREMENT' } という Action が, switch 文の case 'INCREMENT': の部分に対応づけられているのが分かると思います.

state は Read-Only

Reducer に副作用を持たせないようにすることとも関連がありますが, Reducer は Pure な関数で無ければなりません.
つまり Reducer の内部で state.count = 0 だとか state.item.push(newItem) だとか, state を書き換える操作をしてはいけません. 新しい state の値を return すること, これが Reducer の役目です. state の書き換えは Action のレベルで管理しないといけないことなのです.

Provider

さて, ここまで Redux 本体の説明を行ってきました. ここからは, Redux をいかに React に結びつけるか, という react-redux の話が始まります.
Provider は Redux を操作できる React の Component です.

import { Provider } from 'react-redux';

const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

このように Provider で包んでやると, Counter も, その子 Component も, その子 Component も ... 全ての子孫が Redux の (この場合は store という変数名で定義された)Store にアクセスする権限を持ちます.
ただし, これは権限を持っているというだけであり, 実際に操作するにはもうひと段階準備が必要です. それが以下で述べる connect です.

connect 事始め

connect を使っていかに React に Redux を導入するかを説明するために, 先ほど出てきた<Counter /> という Component の実装が具体的に以下のようになっているとしましょう.

class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  decrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  };

  reset = () => {
    this.setState({
      count: 0
    });
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.state.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

export default Counter;

この Counter は, 現時点では, カウンターの値や, カウントを増やしたり減らしたりする関数を, 内部の実装として管理しています. これを Redux を使って外部に委ねたいです.
そのために何をするかというと, あらゆるものをpropsに変換します.
この変換を行うのが mapStateToPropsmapDispatchToProps です.

mapStateToProps

まずは, カウンターの値を Redux に委ねてみましょう.

import { connect } from "react-redux";

class Counter extends React.Component {
  increment = () => {
    /* TODO */
  };

  decrement = () => {
    /* TODO */
  };

  reset = () => {
    /* TODO */
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

export default connect(mapStateToProps)(Counter);

これが connect の第一段階です. 何が変わったのか説明します.

  • this.state.countthis.props.count に変えた. これによりカウンターの値はもはや Counter 内部のものではなくなった.
  • export default Counter;export default connect(mapStateToProps)(Counter); に変えた. つまり, 生の Counter ではなく, connect という関数にラップされた Counter が使われるようになる.
  • connect という関数は第一引数に mapStateToProps という関数をとる. これは Component の state を props に変化させる. 具体的には return されるオブジェクトの key が props の名前に, オブジェクトの value が props の値になる.


上記コード例では /* TODO */ という部分が残っています. まだカウンターの値を変更すること, すなわち Redux の Store を操作することが出来ていないのです. 先に進みましょう.

Action Creator

復習になりますが, Redux の Store の操作の仕方は,

store.dispatch({ type: 'INCREMENT' });

このように Action を Dispatch するのでした.
ところで, .dispatch({ type: 'INCREMENT' }); の部分をそのまま毎回書くのは骨が折れます. そこで, 以下のような Action の定義ファイルを用意してやると良いでしょう. Action を一覧として管理できるというメリットもあります.

actions.js

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const RESET = "RESET";

export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
export const reset = () => ({ type: RESET });

この incrementdecrement など, Action を return する関数を Action Creator と呼びます. これを活用して, 先ほどの /* TODO */ の部分を埋めてみましょう.

import { increment, decrement, reset } from "./actions";

class Counter extends React.Component {
  increment = () => {
    this.props.dispatch(increment());
  };

  decrement = () => {
    this.props.dispatch(decrement());
  };

  reset = () => {
    this.props.dispatch(reset());
  };

これでカウンターの値を操作することまで出来るようになりました!
しかし, まだ出来ることがあります. この dispatch の部分も props に変えてしまおうというのが connect の第二段階 mapDispatchToProps です.

mapDispatchToProps

import { connect } from "react-redux";
import { increment, decrement, reset } from "./actions";

class Counter extends React.Component {
  increment = () => {
    this.props.increment();
  };

  decrement = () => {
    this.props.decrement();
  };

  reset = () => {
    this.props.reset();
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{this.props.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
        <button onClick={this.reset}>×</button>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

const mapDispatchToProps = {
  increment,
  decrement,
  reset
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

mapDispatchToProps を使ってみました. 何が変わったでしょうか.

  • this.props.dispatch(increment());this.props.increment(); という風に書けるようになった.
  • その代わりに connect の第二引数に mapDispatchToProps というオブジェクトが渡されるようになった. これは実体としては, Action Creator の集合である.

以上で react-redux の基本概念の説明が完了しました.

Redux でも非同期処理がしたい!〜 Middleware の登場

ここからは全然理解が及んでいないので概略を述べるに留めます

Reducer は副作用を持ってはいけないと述べましたが, 現実のアプリケーションではデータを引っ張ったり通信したり...と非同期処理が噛みます. 汚い処理をどこかで担当しなければいけません.
Redux 自体には Middlleware という非同期処理を噛ませるレイヤーが用意されていますが, その記述に関しては統一的な手法が定まっているわけではなく, 何らかの独自のやり方で書けるようにせねばならないようです. redux-thunk とか redux-saga とかあるみたいです.

redux-thunk では Action Creator でオブジェクトではなく関数を返す, redux-saga では独立した非同期処理のプロセスを作る ... みたいな雰囲気しか私の理解は及んでいません. Redux は難しいですね...