LoginSignup
17
14

More than 5 years have passed since last update.

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

Posted at

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

Reduxは大変?

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

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

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

Reduxはすごく小さい

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

  • bundle.js : 今回書いたコード。3.6kb
  • vendor.js : ライブラリのコード、といってもReduxしかない。なんと17kb
  • index.html : bundle.jsとvendor.jsを読み込んで実行してるだけのファイル。

肝心のReduxが、たったの17kb。jQueryよりも遥かに小さいです。
更に、webpackを使わない、es5で使った 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 にある通り、本当に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
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();
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>
    `;
  }
}
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;
  }
};
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);
}
reducer.js
import { combineReducers } from 'redux';
import counter from './Actions';

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
14