JavaScript
dom
RFP
es6
Baconjs

なんでもかんでもストリームにするというよりは、使い所を検討したほうが良さそうという話

最近ずっとBacon.jsでコードを書いて遊んでいるのですけど、なんでもかんでもストリームにしてみようと思ってやってみたものの、どうもそうじゃない方がいいケースも多々あるのではないかと思えてきましたので、備忘がてら投下します。

まず前提として、以下のような2つのモジュールなjsがあるのですが

dom.js
/**
 * 指定したタグのDOMを生成して、CSSクラスを設定したうえで返します。
 * @param {string} tagName タグ名
 * @param {string} className CSSクラス名
 * @return {object} 生成したDOM
 */
export const create = (tagName, className) => {
  const $dom = document.createElement(tagName);
  $dom.className = className;
  return $dom;
};

/**
 * テンプレート文字列をDOMに変換して返します。
 * @param {string} template テンプレート文字列
 * @return {object} DOM
 * NOTE: templateは副作用的に与えることを想定しています。
 */
export const fromTemplate = template => {
  const $dom = document.createElement('div');
  $dom.innerHTML = template;
  return $dom.firstElementChild;
};

/**
 * 指定した親要素内からDOMを探して、最初のものを返します。
 * @param {object} $root 検索の起点となる親のDOM
 * @param {string} selector 検索に使うセレクタ
 * @return {object} 見つかったDOM
 *
 */
export const find = ($root, selector) =>
  $root.querySelector(selector);
player.js
/**
 * プレイヤーのDOMを返します。
 * @param {number} number 任意の番号
 * @return {object} プレイヤーのDOM
 */
const getPlayer = number => {
  const $player = dom.fromTemplate(template); // ここに記載はないですけど、モジュール先頭とかでHTMLファイルをインポートしています。
  dom.find($player, 'h2').textContent += number;
  $player.classList.add(`player${number}`);
  return initSound(initButtons($player), number);
};

/**
 * 指定した数のプレイヤーのDOMを配列で返します。
 * @param {number} from 開始番号
 * @param {number} to 終了番号
 * @return {array} プレイヤーのDOMの配列
 */
export const getPlayers = (from, to) => {
  const array = [getPlayer(from)];
  return from < to
    ? array.concat(getPlayers(from + 1, to))
    : array;
};

モジュールはここまで。

--

本題はここからなのですが、まず最初はDOM操作を普通に書いてみたのがこちら。

DOM操作で書いた場合
const $app = dom.find(document, '.app');
const $container = dom.create('div', 'container');

$app.appendChild($container);

player.getPlayers(1, 5)
  .forEach($player => $container.appendChild($player));

その後、試しに意地でストリームだけでやってみようとしたところ、長いうえに可読性もそんなに良くないコードになってしまいました。

ストリームの合成で書いた場合
const $app = dom.find(document, '.app');

const containerProperty = Bacon.constant(dom.create('div', 'container'));
const playersArrayStream = Bacon.fromArray(player.getPlayers(1, 5));

const appendPlayerToContainer = ($container, $player) => {
  $container.appendChild($player);
  return $container;
};

const appendContainerToApp = $container => $app.appendChild($container);

containerProperty
  .sampledBy(playersArrayStream, appendPlayerToContainer)
  .skipDuplicates()
  .onValue(appendContainerToApp);

というわけで、なんでもかんでもストリームにするというよりは、使い所をうまく見つけてやるのが良さそうだと思いました。とはいえ、まだまだ私はストリームとかリアクティブとかBacon.jsとか勉強中の身なので、もっとうまい書き方があるよという方おりましたら教えてください。