俺のReduxがフレームワークなわけがない

今回は、いかにReduxがフレームワークではなくライブラリであるか、
というのを感じてもらう内容になっています。

# Reduxは大変？
皆さんReduxに興味はありますか？
Reduxの記事を読んでみたり、実際に試したことがある人は

「たくさん覚えるものがある」
「結構難しい」
「React使わないといけないし使い方が限定される」

そんな印象を受けたんじゃないでしょうか。
私は仕事でReact+Reduxを使っているんですが、なかなか難しいなぁと感じています。
ES2015、React、Webpack、Babel、そのほかのReduxをサポートする様々なライブラリ・・・
多くのものが合わさった結果、「Reduxは大変だ」という印象を抱くに至りました。

# Reduxはすごく小さい
余計なものが多くあるせいで複雑に感じるRedux。
ですが、**Reduxだけ**でコードを書くと、こんな風になります。

* [bundle.js](https://github.com/nabepon/redux-is-library/blob/master/dist/assets/bundle.js) : 今回書いたコード。3.6kb
* [vendor.js](https://github.com/nabepon/redux-is-library/blob/master/dist/assets/vendor.js) : ライブラリのコード、といってもReduxしかない。なんと17kb
* [index.html](https://github.com/nabepon/redux-is-library/blob/master/dist/index.html) : bundle.jsとvendor.jsを読み込んで実行してるだけのファイル。

肝心のReduxが、たったの17kb。jQueryよりも遥かに小さいです。
更に、webpackを使わない、es5で使った [redux.min.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/redux.min.js) は、**たったの7kb！！**

サイズが小さいからフレームワークではなくライブラリなのだ、とは言えませんが、
「ただのライブラリ感」は感じられませんか？

# Reduxだけでコードを書いてみた
書いてみました
ES2015: https://github.com/nabepon/redux-is-library/tree/master/src
ES5: https://github.com/nabepon/redux-is-library/tree/master/es5

[package.jsonのdependencies ](https://github.com/nabepon/redux-is-library/blob/master/package.json)にある通り、本当にReduxしか使っていません。
それでも、Reduxはちゃんと使用できます。

実際に動くページがこちらです
ES2015: https://nabepon.github.io/redux-is-library/dist/
ES5: https://nabepon.github.io/redux-is-library/es5/

超シンプルですが、Reduxの基本的な機能は押さえています。
非同期処理をする場合はmiddlewareとPromiseでもう少し複雑になりますが、
逆にいうと、Reduxが行うのは全体でこれくらいのことしかありません。
いかにReduxが小さいかを感じることができるでしょうか。

# 今回のサンプルの役割と、示したかったもの

* 超最小限の構成なので、これがReduxを理解する助けになれば嬉しい
* Reduxの機能自体は本当に小さい。
* 7kbしかないので気軽に追加できる。
* 実は複雑な開発基盤は必要としない。
* 部分的にReduxで作ることも可能。
* そのため、既存サイトにも部分的な導入が可能。
* 機能的にはBackbone.jsとかの立ち位置に近い。

**Reduxは手軽にライブラリとして使える、その可能性を感じてもらえたら嬉しいです。**

.
.
.


# 実際のコード
コメントを充実させたので、コードを載せておきます。

* ES2015はわからん！って方は以下を参照していただければと思います。
  * [index.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/index.js)
  * [Component.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/Component.js)
  * [Actions.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/Actions.js)
  * [utils.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/utils.js)
  * [reducer.js](https://github.com/nabepon/redux-is-library/blob/master/es5/assets/reducer.js)

ES2015が大丈夫な方は以下をどうぞ

```js:index.js
import { createStore } from 'redux';
import reducer from './reducer';
import Component from './Component';

class App {
  constructor(){
    this.store = createStore(reducer, {});
    this.el = document.querySelector('#app');
  }

  counterPage(){
    const component = new Component(this.store);
    this.el.appendChild( component.mount() );
  }
}

window.app = new App();
```

```js:Component.js
import {mapDispatchToActions, hasClass} from './utils';
import * as Actions from './Actions';

export default class Component {
  /**
   * storeとactionを他メソッドから参照できるよう自身に追加する
   * またrenderとhandlerのthis参照が変わってしまうのでbindしておく
   */
  constructor(store) {
    this.store = store;
    this.actions = mapDispatchToActions(this.store, Actions);
    this.render = this.render.bind(this);
    this.handler = this.handler.bind(this);
  }

  /**
   * Componentを追加する処理
   * actionを実行した時にrenderが走るよう、storeの変更を監視する
   * 自身のelementを作成し、イベントハンドラをaddEventListenerする
   * returnした自身のelementは、mountの呼び出し元でdom構築に使う
   */
  mount() {
    this.unsubscribe = this.store.subscribe(this.render);
    this.el = document.createElement("div");
    this.el.addEventListener("click", this.handler, false);
    this.render();
    return this.el;
  }

  /**
   * Componentを削除する処理
   * removeEventListenerし、自身のelementを削除する
   */
  unmount() {
    this.unsubscribe();
    this.el.removeEventListener("click", this.handler, false);
    this.el.parentNode.removeChild(this.el);
  }

  /**
   * イベントハンドラ
   * 要素ごとにaddEventListenerは大変なのでしない
   * mountのthis.el.addEventListenerで一括制御する
   */
  handler(e) {
    const state = this.store.getState().counter;
    if(hasClass(e, "js-increment")){
      this.actions.increment(state.count);
    }
    if(hasClass(e, "js-decrement")){
      this.actions.decrement(state.count);
    }
  }

  /**
   * html文字列を生成し、自身を書き換える関数
   * ここに他Componentを追加したい場合、mount関数内で
   * this.child = new ChildComponent().mount();
   * のようにインスタンスを作っておき、this.el.innerHTMLの後に
   * this.el.querySelector('.childContainer').appendChild(this.child)
   * のような追加処理を書く
   */
  render() {
    const state = this.store.getState().counter;
    this.el.innerHTML = `
      <div>${state.count}</div>
      <button class="js-increment">increment</button>
      <button class="js-decrement">decrement</button>
    `;
  }
}
```

```js:Actions.js
/******************************************
 * 定数
 * switch caseでtype判定するためのお作法
 ******************************************/
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

/******************************************
 * Actions
 * stateを変更する関数
 * typeとpayloadはお作法
 ******************************************/
export function increment(current) {
  return {
    type: INCREMENT,
    payload: {
      count: current + 1
    },
  }
}

export function decrement(current) {
  return {
    type: DECREMENT,
    payload: {
      count: current - 1
    },
  }
}

/******************************************
 * 初期値のお作法
 ******************************************/
const initialState = {
  count: 0
};

/******************************************
 * reducer
 * stateを作成、変更するための薄い関数
 * store.dispatch(increment()) とすることで
 * Actionとreducerが紐づきstateが更新される
 ******************************************/
export default function reducer(state = initialState, { type, payload }) {
  switch (type) {
    case INCREMENT:
      return {
        ...state,
        ...payload
      };

    case DECREMENT:
      return {
        ...state,
        ...payload
      };

    default:
      return state;
  }
};
```

```js:utils.js
import { bindActionCreators } from 'redux';

/**
 * incrementやdecrementは、実行するとただのobjectを返すだけ。
 * この処理を通ることでstore.dispatchと紐づき、stateも変更されるようになる
 */
export function mapDispatchToActions(store, Actions){
  const dispatch = store.dispatch;
  const ret = {...Actions};
  for(const i in ret){
    ret[i] = bindActionCreators(ret[i], dispatch);
  }
  return ret;
}

/**
 * jQueryのhasClassに相当する関数
 */
export function hasClass(e, name){
  return e.target.classList.contains(name);
}
```

```js:reducer.js
import { combineReducers } from 'redux';
import counter from './Actions';

/**
 * state構造を定義する。
 * stateの種類を増やしたい場合はここに追加していく。
 * store.getState().counter が使えるようになるための処理がここ。
 */
export default combineReducers({
  counter,
});
```