Takahiro Octopress Blog

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

ReduxとReactを組み合わせてみよう!〜iOSエンジニアが苦しんだReduxの基礎(2)〜

| Comments

はじめに

早くも2017年になってしまいましたね。
本年もよろしくお願い致します。

今回はiOSエンジニアが苦しんだReduxの基礎(1)の続きを書いていきたいと思います。
前回はReduxのみを利用したWebサイトの実装について話をしましたが、今回はReactと組み合わせて行きたいと思います。
では早速見ていきましょう。

Counterサンプルで学ぼう!

公式ReduxページのExampleの先頭に書かれている Counter を見ていきましょう。
実現するWebサイトは Counter Vanilla と同じで下図のようになります。

Counter Example画面

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

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

前回と異なるのはViewに React を利用しているという点です。
では1つ1つ見ていきましょう。

フォルダ構成

まずはフォルダ構成を見ていきます。
(説明のために一部変更を加えています。)

1
2
3
4
5
6
7
8
9
10
11
12
counter
├── public
     └── index.html
└── src
     ├── index.js
     ├── actions
          └── index.js
     ├── reducers
          └── index.js
     ├── components
          └── Counter.js
     └── node_modules

Actions

今回Actionsactions/index.jsにまとめています。
ActionsAction Creatorsについて復習しておきます。

  • Actions
    • 何をするアクションなのかを表すオブジェクト
    • typeプロパティを必ず持つ
  • Action Creators
    • Actionを作成するメソッド
1
2
3
4
5
6
7
8
9
10
// actions/index.js
const INCREMENT = { type: 'INCREMENT' }
const DECREMENT = { type: 'DECREMENT' }

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

Reducers

今回Reducersreducers/index.jsにまとめています。
Reducersについても復習しておきます。

  • ActionStateから新たなStateを作成して返す
  • ポイントはStateを更新するのではなく、 新しく作成したState を返すということ
1
2
3
4
5
6
7
8
9
10
11
12
// reducers/index.js
export default (state = 0, action) => {
  // stateの初期値を0に設定
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

Components

今回はReactを利用するので、描画部分を表現するComponentsを作成する必要があります。
ComponentsにはPresentational ComponentContainer Componentの2種類が存在します。

  • Presentational Component
    • 画面の描画を担当するComponent
  • Container Component
    • Presentational Componentにデータやコールバックを渡すComponent

本記事のExampleは簡単なため、Presentational Componentのみ利用しています。
Presentational Componentcomponents/Counter.jsが該当します。
ソースコードは下記の通りです。

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
// components/Counter.js
import React, { Component, PropTypes } from 'react'

class Counter extends Component {
  static propTypes = {
    value: PropTypes.number.isRequired,
    onIncrement: PropTypes.func.isRequired,
    onDecrement: PropTypes.func.isRequired
  }

  incrementIfOdd = () => {
    if (this.props.value % 2 !== 0) {
      // valueが奇数の場合のみonIncrementを実行
      this.props.onIncrement()
    }
  }

  incrementAsync = () => {
    // 1秒後にonIncrementを実行
    setTimeout(this.props.onIncrement, 1000)
  }

  render() {
    // 描画処理
    const { value, onIncrement, onDecrement } = this.props
    return (
      <p>
        Clicked: {value} times
        {' '}
        <button onClick={onIncrement}>
          +
        </button>
        {' '}
        <button onClick={onDecrement}>
          -
        </button>
        {' '}
        <button onClick={this.incrementIfOdd}>
          Increment if odd
        </button>
        {' '}
        <button onClick={this.incrementAsync}>
          Increment async
        </button>
      </p>
    )
  }
}

export default Counter

Counter Componentで最も重要なポイントは、
『描画にはvalue, onIncrement, onDecrementの3つが必須』という点です。

Store

今回Storesrc/index.jsにまとめられています。
Storeについても復習しておきましょう。

  • アプリ内で必ず1つの存在
  • アプリの状態を管理する
  • Stateを更新するためのdispatchを提供する
    • 言い換えればdispatch(action)をすることでStoreStateの変更を知らせられる
  • Stateの状態を追えるようにsubscribeを提供する
    • 言い換えればsubscribe(listener)をすることでlistenergetStateを通してStateの状態を取得できる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './components/Counter'
import counter from './reducers'
import { increment, decrement } from './actions'

const store = createStore(counter)
const rootEl = document.getElementById('root')

const render = () => ReactDOM.render(
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch(increment())}
    onDecrement={() => store.dispatch(decrement())}
  />,
  rootEl
)

render()
store.subscribe(render)

少し詳しくポイントを見ていきましょう。
冒頭で必要なモジュールを読み込んでいます。

1
2
3
4
5
6
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import Counter from './components/Counter'        // Component
import counter from './reducers'                  // Reducer
import { increment, decrement } from './actions'  // Action

そして、下記のようにstoreを作成しています。

1
2
// カウントアップ・ダウンのReducerを引数にStoreを生成
const store = createStore(counter)

また、前回はHTMLに全て構成を書いていましたが、今回はReactを利用していますので、下記のように描画をしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// id="root"のDOMを取得
const rootEl = document.getElementById('root')

// 描画メソッド
const render = () => ReactDOM.render(
  {/* Counter Componentの呼び出し */}
  <Counter
    value={store.getState()}
    onIncrement={() => store.dispatch(increment)}
    onDecrement={() => store.dispatch(decrement)}
  />,
  rootEl
)

// 初期描画処理
render()

Counter Componentの3つの必須propTypesに下記を渡しています。

  • value: Stateの値(store.getState())
  • onIncrement: StoreStateの増加を通知(() => store.dispatch(increment))
  • onDecrement: StoreStateの減少を通知(() => store.dispatch(decrement))

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

その後、render()で描画処理を実行しています。

最後に、listener処理です。

1
2
// subscribeの第一引数にrenderメソッドを指定
store.subscribe(render)

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

まとめ

さて、前回の素のReduxから、『ReactとReduxをそれぞれ素で利用する』ところまで進んできました。
ここまで割りとすんなり理解できたのであれば、connect()を利用したReact&Reduxの実装の理解までもう少しだと思います。
Reduxの理解を促進する上で重要なのは、

  • ユーザの操作結果をStoreに伝えること
  • Storeの変更に応じて画面を再描画すること

だと思っています。
ここまでの流れさえ理解できてしまえば、応用的な内容も躓かずに理解できるはずです。
さて次回はいよいよconnect()を利用したReact&Reduxの実装について見ていきます。

Comments