LoginSignup
5
5

More than 5 years have passed since last update.

Redux と仮想 DOM ライブラリの Snabbdom を組み合わせる

Last updated at Posted at 2016-03-15

記事を書いた動機

  • jQuery や Backbone.js を使っており、仮想 DOM への対応を少しずつ進めたい。
  • これまで使ってきたツールとの比較をしたい。
  • 仮想 DOM を導入することでどんなことが期待できるのか、自分が納得できる説明をできるようになりたい。

Redux を使う理由

アプリケーションの状態管理のために Redux を使う理由は Event Dispatcher (Event Bus もしくは Event Emitter) に相当する機能が組み込まれていることです。

subscribe メソッドによって、Model の更新を監視して、View を自動的に更新することができます。View の自動更新機能は仮想 DOM ライブラリを使う上で必要不可欠です。もし自動更新機能がなければ、イベントごとに View 更新のメソッドを呼び出さなければなりません。仮想 DOM のおかげで、View を更新する際に Model の個別の値を監視しなくてすみます。

Redux および仮想 DOM を採用する共通の目的は Model と View の責務を明確にして分離することです。

Event Dispatcher と比較すると、イベントはアクションという名前に置き換わっています。アクションの実行を実行するには dispatch メソッドを使います。dispatch メソッドの引数はオブジェクトなので、アクション以外の情報を渡すこともできます。

アクションの規約として非公式の Flux Standard Action (翻訳) があります。redux-actions はアクションを生成するためのユーティリティメソッドを提供します。

Event Dispatcher をもとに Model を自分でつくる場合と比べて、配布のためのパッケージ管理をしたり、マニュアルをつくらなくてすみます。

比較の対象として Backbone.js の Model を挙げることができます。Redux の場合、View や Router のライブラリが含まれないので、さまざまなライブラリと組み合わせることができます。

ほかの違いとして、ユーザーの入力と Model の関係式を管理するためのしくみが Model に内包されていることが挙げられます。

ユーザーの入力と Model の関係をあらわすのに reducer と呼ばれる関数を使います。Model を生成するには、ファクトリメソッドである createStore で reducer を指定します。reducer のロジックが大きくなって複数の reducer に分割するために、複数の reducer を合成する combineReducers が用意されています。

非同期処理への対応には redux-thunkredux-saga といったミドルウェアが開発されています。

ほかに Redux のアーキテクチャの参考になった Elm アーキテクチャを理想とする開発ができるようにする取り組みとして、redux-loopredux-effectsredux-elm などが挙げられます。

React ではなく Snabbdom を使う理由

React は仮想 DOM とコンポーネント API を統合したライブラリですが、アプリの状態管理を Redux に任せ、コンポーネントをステートレスに保ち、ストアの状態をそのまま反映させる方針をとる場合、React が提供するさまざまなコンポーネント API が不要になります。

仮想 DOM ライブラリに関して、処理速度の改善や API のありかたをめぐり、さまざまな開発者が取り組んでいます。Snabbdom を選ぶ理由は処理速度の速さと API のシンプルさです。くわしくは Backbone.js で書かれたアプリに仮想 DOM ライブラリの Snabbdom を導入する の記事をご参照ください。

コードの例

ES5 + const で書きました。ES2015 (ES6) で導入された const は主要なブラウザーでサポートされています。

Hello World

const { createStore } = require('redux');
const snabbdom = require('snabbdom');
const patch = snabbdom.init([
  require('snabbdom/modules/class'),
  require('snabbdom/modules/props'),
  require('snabbdom/modules/style'),
  require('snabbdom/modules/eventlisteners'),
]);
const h = require('snabbdom/h');
const { div, p } = require('hyperscript-helpers')(h);

function createNode(Model) {
  return div([
    p(Model.text)
  ]);
}

function render(oldVnode, Model) {
  var newVnode = createNode(Model);
  return patch(oldVnode, newVnode);
}

// http://stackoverflow.com/a/14947838/531320
const fromId = document.getElementById.bind(document);
var oldVnode = fromId('container');

var store = createStore(function(state, action) {
  return action.hasOwnProperty('type') ? action.text : '';
});

store.subscribe(function() {
  oldVnode = render(oldVnode, store.getState());
});

store.dispatch({
  type: 'CONTENT',
  text: 'Hello World'
});

クリックカウンター

const { createStore } = require('redux');
const snabbdom = require('snabbdom');
const h = require('snabbdom/h');
const { div, p, button } = require('hyperscript-helpers')(h);

const patch = snabbdom.init([
  require('snabbdom/modules/class'),
  require('snabbdom/modules/props'),
  require('snabbdom/modules/style'),
  require('snabbdom/modules/eventlisteners'),
]);

function createNode(count) {
  return div([
    p([String(count)]),
    button('#plus', 'プラス'),
    p([' ']),
    button('#minus', 'マイナス'),
  ]);
}

function render(oldVnode, count) {
  var newVnode = createNode(count);
  return patch(oldVnode, newVnode);
}

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}

const store = createStore(counter);

// http://stackoverflow.com/a/14947838/531320
const fromId = document.getElementById.bind(document);

var oldVnode = fromId('container');
var count = 0;

oldVnode = render(oldVnode, count);

store.subscribe(function() {
  oldVnode = render(oldVnode, count);
});

fromId('plus').addEventListener('click', function() {
  store.dispatch({ type: 'INCREMENT' });
});
fromId('minus').addEventListener('click', function() {
  store.dispatch({ type: 'DECREMENT' });
});
5
5
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
5
5