今回は、いかに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はわからん!って方は以下を参照していただければと思います。
ES2015が大丈夫な方は以下をどうぞ
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();
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>
`;
}
}
/******************************************
* 定数
* 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;
}
};
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);
}
import { combineReducers } from 'redux';
import counter from './Actions';
/**
* state構造を定義する。
* stateの種類を増やしたい場合はここに追加していく。
* store.getState().counter が使えるようになるための処理がここ。
*/
export default combineReducers({
counter,
});