強まっていこう

あっちゃこっちゃへ強まっていくためのブログです。

ガタガタ抜かさず Redux の使い方だけ簡単に教えろ!

決して、React や Redux の使用を推奨するものでは無い事をご了承願いたい。

React/Redux なるものをヨダレ垂らした noob エンジニアがググった結果がいっぱい出てくる = 流行っている、と言う無邪気な理由だけで導入。
案の定大トラブルとなり、そいつは抜け、火消しとして入ったおっちゃん達が「なんっっじゃこりゃ・・・」と愚痴りつつ調べてもゴミみたいな内容しか HIT せず「こんなもん時間のありあまった暇人が使うもんだろ!こっちは仕事さっさと終えて子供とスマブラせなあかんねん!ぶち転がすぞ!!!」
と発狂しないようにするための処方箋(主に俺向け)。

これらは大量のエンジニアが暇を持て余した結果、現場に故意に混乱をもたらし知ってるやつがドヤ顔で相手を煽るための代物であり、決して使うべからず。
決して幸せにはなれない。多くの FB エンジニアもこっそりそう思っているはず。互換性の無さでエンジニアを殺しに来る Angular と Vue よりマシかな?程度。
嫌々ながら使うハメになってしまった可愛そうな人向けエントリーなので React/Redux ファンボーイは帰れ。

前置きが長い。本題に移る。React はわかっている事前提。

Redux の基本イメージは、Component に status と dispatcher をまとめてくっつけるもの。
status は Component をまたいで変化可能なグローバル変数的なもので、その status を変えるのが dispatcher だと思えば良い。
こう言うものを props に1つ1つ attribute として渡してると多層のコンポーネントになった時発狂するのでそれを回避するのが主目的。
くっつけたものはすべて props 経由で取得。

オブジェクトにオブジェクトをまたぐグローバルなメンバー変数とプライベートなメソッドをつける狂気的なものだと思ったら理解しやすい。
何じゃそりゃ、破綻するだろそんなもの、と思うかもしれないがその通りでちゃんとやらないと破綻する。

たったこれだけの事をやる代物に理解を阻害する素敵要素がふんだんに散りばめられている。

まずコードを見てもらいたい。

import axios from 'axios'
import React from 'react'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'

const status = {
  value: 'XXX',
  time: '---',
}

const Main = connect()(props => {
  return (<div>
    <Input />
    <Display />
  </div>)
})

const Display = connect(state => state)(props => {
  return (<div>
    <p>Display: {props.value}</p>
    <p>Time: {props.time}</p>
  </div>)
})

const Input = connect(
  state => state,
  dispatch => {
    return {
      send: arg => {
        dispatch({
          type: 'send',
          payload: arg,
        })
      },
      hoge: () => {
        dispatch({
          type: 'hoge'
        })
      },
      getTime: async (arg) => {
        const res = await axios.get('https://ntp-a1.nict.go.jp/cgi-bin/time')
        dispatch({
          type: 'getTime',
          payload: arg + res.data
        })
      }
    }
  })(props => {
  let input = ''
  return (<div>
    <input type='text' ref={ref => (input = ref)} defaultValue='' />
    <button onClick={() => { props.send(input.value); input.value = '' }}>Send</button>
    <button onClick={() => props.hoge()}>Hoge</button>
    <button onClick={() => props.getTime('NOW=')}>getTime</button>
  </div>)
})

const store = createStore((state, action) => {
  switch(action.type) {
    case 'send':
      return { ...state, value: action.payload }
    case 'hoge':
      return { ...state, value: 'HOGE' }
    case 'getTime':
      return { ...state, time: action.payload }
    default:
      return state
  }
}, status)

export default (props) => {
  return <Provider store={store}><Main /></Provider>
}

Input で値を入力し、Display で表示、それを束ねるのが Main。 え?たったそれだけの事をやるのにこんなに大げさなコードが?と思ったら正常。
これが React/Redux の魅力。仕事が増えてラッキーラッキー。

connect が Component に対して status と dispacher を bind するためのもの。

dispatcher では、Object を返しているが、key が いわゆるプライベートメソッド名で props 経由で叩けるようになる。
メソッド内部では dispatch 関数を叩くが、ここからが混乱を招く謎要素の肝でありマジキモい。常識を捨てろ系。

こいつを説明するために store の説明から。

store は、status を Component に渡すためのもので Provider を使って各 Component に渡す。

コードの中に createStore ってのがあると思うが、第2引数は status の初期値。第1引数には何やら関数がぶら下がっているのが見えると思う。
こいつは、status を更新するためのもの。reducer と呼ばれている。

は?dispacher で更新するんぢゃねぇの?と思うだろうが、違うんだなこれが。

dispatcher 内部で dispach に渡している Object は通称 action。

dispach を呼ぶと reducer が呼ばれ action がそのまま渡る。

createStore((state, action) => ....) の state は現在の statusで、action はまさに dispach で渡した action。

この reducer が返した値が、新しい status として扱われる。

で、何も考えないと1つの更新しか行えないから、action.type で処理を分けれるようにしているだけと言う。

これを真顔で「これぞフレームワークだ」と言い出す神経に脱帽。

type と言うキーを使っちゃってるから、更新内容は payload に詰めましょうね、と言う暗黙の世界。そう、全てが狂気。だからこそ普通の人間には理解しづらい。

こんなグズグズだから、世の中は action.type の value を無駄に定数にしてみたり Object を返すだけなのに御大層に actionCreator なんてものをいちいち用意したりしてより混沌化している。

action なんて本来隠蔽すべきもの。外に出しておく必要性が一切無い。

そこらあたりを隠蔽したものが下。参考されたし。

import axios from 'axios'
import React from 'react'
import { Provider } from 'react-redux'

const gStatus = {
  value: 'XXX',
  time: '---',
}
const gReducer = {}
const gDispatcher = {}
const gAsyncDispatcher = {}

gReducer.send = (state, value) => {
  return { ...state, value }
}
gReducer.hoge = (state) => {
  return { ...state, value: 'HOGE' }
}
gReducer.getTime = (state, time) => {
  return { ...state, time }
}

gDispatcher.send = args => args
gDispatcher.hoge = () => {}
gAsyncDispatcher.getTime = async (arg) => {
  const res = await axios.get('https://ntp-a1.nict.go.jp/cgi-bin/time')
  return arg + res.data
}

const Main = bindComponent(props => {
  return (<div>
    <Input />
    <Display />
  </div>)
})

const Display = bindComponent(props => {
  return (<div>
    <p>Display: {props.value}</p>
    <p>Time: {props.time}</p>
  </div>)
})

const Input = bindComponent(props => {
  let input = ''
  return (<div>
    <input type='text' ref={ref => (input = ref)} defaultValue='' />
    <button onClick={() => { props.send(input.value); input.value = '' }}>Send</button>
    <button onClick={() => props.hoge()}>Hoge</button>
    <button onClick={() => props.getTime('NOW=')}>getTime</button>
  </div>)
}, gDispatcher, gAsyncDispatcher)

const store = makeStore(gStatus, gReducer)
export default (props) => {
  return <Provider store={store}><Main /></Provider>
}

// 以下本来外に出すべき関数(記事の便宜上ここにおいている)
export const makeStore = (status, reducer) => {
  return createStore((state, action) => {
    if (reducer[action.type]) {
      return reducer[action.type](state, action.payload)
    }
    else {
      return state
    }
  }, status)
}

export const bindComponent = (component, dispatcher = {}, asyncDispatcher = {}) => {
  return connect(
    state => state,
    dispatch => {
      const map = {}
      for (let type in dispatcher) {
        map[type] = state => dispatch({
          type,
          payload: dispatcher[type](state)
        })
      }
      for (let type in asyncDispatcher) {
        map[type] = async state => dispatch({
          type,
          payload: await asyncDispatcher[type](state)
        })
      }
      return map
    }
  )(component)
}

(理想は reducer なんて消滅させ dispatcher で現在の status が取れて、新しい status を返すだけにしたいんだが)