Takahiro Octopress Blog

-1から始める情弱プログラミング

Reduxの基礎を学ぼう!〜iOSエンジニアが苦しんだReduxの基礎(1)〜

| Comments

はじめに

今年も残すところ1日となりましたね。
筆者にとって2016年は新しいことだらけの1年となりました。
技術的にもこれまで触ってこなかったものに挑戦する機会を得ることができました。
その1つは本日のお題である Redux です。
実は現在進行系で理解に苦しんでいる途中なので、休暇中に理解を促進する意味も込めて記事を書きたいと思います。

Reduxは難しい!?

ここ最近、iOSエンジニアとして活動してきた筆者にとって なぜReduxが難しいと感じたのか について語りたいと思います。
結論から言うと、 ReactとReduxを組み合わせた場合の実装から入ったため 難しく感じたのではないかと思っています。
具体的には、ReactとReduxの公式ページや様々な紹介ページを読むと必ず出てくる connect() がかなり難しいと感じました。

プロジェクトに参画するタイミングによっては悠長に勉強をしている場合ではないと思いますが、
まずは素のReduxを理解することが最も近道だと実体験として感じました。

では、素のReduxはどうやって学べば良いのでしょうか?
Reduxで検索をしてもほとんどの場合、Reactと一緒に使われている記事しか出てこないと思います。
答えは簡単です!
(当たり前ではあるのですが、)公式のExampleを勉強すれば良いのです。

Counter Vanilla サンプルで学ぼう!

公式ReduxページのExampleの先頭に書かれている Counter Vanilla を見ていきましょう。
ソースコードは下記のようになっています。
(少々、Reduxの原則と照らし合わせられるようにソースコードを改変しています。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Redux basic example</title>
    <script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
  </head>
  <body>
    <div>
      <p>
        Clicked: <span id="value">0</span> times
        <button id="increment">+</button>
        <button id="decrement">-</button>
        <button id="incrementIfOdd">Increment if odd</button>
        <button id="incrementAsync">Increment async</button>
      </p>
    </div>
    <script>
      const INCREMENT = { type: 'INCREMENT' };
      const DECREMENT = { type: 'DECREMENT' };

      function increment() {
        return INCREMENT;
      }
      function decrement() {
        return DECREMENT;
      }

      function counter(state, action) {
        if (typeof state === 'undefined') {
          return 0
        }

        switch (action.type) {
          case 'INCREMENT':
            return state + 1
          case 'DECREMENT':
            return state - 1
          default:
            return state
        }
      }

      var store = Redux.createStore(counter)
      var valueEl = document.getElementById('value')

      function render() {
        valueEl.innerHTML = store.getState().toString()
      }

      render()
      store.subscribe(render)

      document.getElementById('increment')
        .addEventListener('click', function () {
          store.dispatch(increment())
        })

      document.getElementById('decrement')
        .addEventListener('click', function () {
          store.dispatch(decrement())
        })

      document.getElementById('incrementIfOdd')
        .addEventListener('click', function () {
          if (store.getState() % 2 !== 0) {
            store.dispatch(increment())
          }
        })

      document.getElementById('incrementAsync')
        .addEventListener('click', function () {
          setTimeout(function () {
            store.dispatch(increment())
          }, 1000)
        })
    </script>
  </body>
</html>

ブラウザに表示される画面は下記のようになります。
Counter Example画面

実装されている機能としては下記の4つになります。

  • 「+」ボタンを選択するとClick数が+1される
  • 「-」ボタンを選択するとClick数が-1される
  • 「Increment if odd」ボタンを選択するとClick数が奇数のときのみ+1される
  • 「Increment async」ボタンを選択すると1秒後にClick数が+1される

これだけを見せられても実装内容がよくわからない(かもしれない)ですよね?
そこで1つ1つReduxの原則と照らし合わせつつ見ていきましょう。

Actions

Actionsとは

  • 何をするアクションなのかを表すオブジェクト
  • typeプロパティを必ず持つ

です。
Exampleでは、下記に当たります。

1
2
const INCREMENT = { type: 'INCREMENT' }
const DECREMENT = { type: 'DECREMENT' }

Action Creators

Action Creatorsとは

  • Actionを作成するメソッド

です。
Exampleでは、下記に当たります。

1
2
3
4
5
6
function increment() {
  return INCREMENT
}
function decrement() {
  return DECREMENT
}

Reducers

Reducersとは

  • ActionStateから新たなStateを作成して返す
  • ポイントはStateを更新するのではなく、 新しく作成したState を返すということ

です。
Exampleでは、下記に当たります。
※Counter Exampleのため、stateカウント数 を表します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function counter(state, action) {
  if (typeof state === 'undefined') {
    // 初期値は0として返却する
    return 0
  }

  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

Store

Storeとは

  • アプリ内で必ず1つの存在
  • アプリの状態を管理する
  • Stateを更新するためのdispatchを提供する
    • 言い換えればdispatch(action)をすることでStoreStateの変更を知らせられる
  • Stateの状態を追えるようにsubscribeを提供する
    • 言い換えればsubscribe(listener)をすることでlistenergetStateを通してStateの状態を取得できる

です。
Exampleでは下記のようにstoreを作成しています。

1
2
// Storeを作成するためのcreateStoreメソッドの第一引数にReducerを渡す
var store = Redux.createStore(counter)

今回はCounter Exampleであるため、

  • 「+」ボタンを選択したら「+1」された結果が表示される
  • 「-」ボタンを選択したら「-1」された結果が表示される

ようになっています。
よって、「+ / -」ボタンをクリックしたタイミングで

  • Storeにstateの変更を知らせる
  • stateの変更を結果として描画に反映させる

必要があります。
これはStore項目の冒頭で書いたdispatchlistenerを利用することで達成できます。

まずはdispatchから見ていきましょう。

1
2
3
4
5
6
7
8
9
10
11
// 「+」ボタンを選択した場合
document.getElementById('increment')
  .addEventListener('click', function () {
    store.dispatch(increment())
  })

// 「-」ボタンを選択した場合
document.getElementById('decrement')
  .addEventListener('click', function () {
    store.dispatch(decrement())
  })

上記のようにクリックしたタイミングでstore.dispatchAction Creatorsであるincrement()およびdecrement()で作成したActionsを渡しています。
こうすることで、『 Storeにstateの変更を知らせる 』ことができます。

次にlistenerから見ていきましょう。

1
2
3
4
5
6
7
8
9
10
// Click数を示すDOMの取得
var valueEl = document.getElementById('value')
// 最新のstateの状態をClick数に反映するメソッド
function render() {
  valueEl.innerHTML = store.getState().toString()
}
// 初期状態を表示するために実行
render()
// subscribeの第一引数にrenderメソッドを指定
store.subscribe(render)

上記のように、store.subscriberenderメソッドを渡すことで、dispatch実行してstateの状態が変化したときに、毎回renderメソッドが実行されることになります。
こうすることで、『 stateの変更結果として描画に反映させる 』ことができます。
subscribestateの状態を監視する役割を持っていることがわかると思います。

まとめ

さて如何でしたでしょうか?
今回はReduxの本質を理解するために、あえてReactを利用するExampleは選択しませんでした。
「React&Reduxが全然わからん!!」という方はゆっくりと時間をとってReduxからチャレンジしてみることをオススメします。
次回はReactとの組み合わせを見ていきたいと思います。

参考:

Comments