へっぽこびんぼう野郎のnewbie日記

けろけーろ(´・ω・`)! #vZkt8fc6J

Reduxのソースを読んでみたので、Reactを使わずにReduxを説明してみた(Redux入門)

Redux読んだ

会社の勉強会で発表したので共有も兼ねて。スライドはこれ↓


Reduxをざっくり読んでみた。

この記事の前提知識

EcmaScriptがあることは知っている
ES6を使うにはBabelがオススメ!
npmでインストールするのとかpackage.jsonとかは知ってる。

それがわからない場合は、たぶんやり方すらわからないはずなので、こっちを見てくれると嬉しいです↓
heppoko.hatenadiary.jp

要はこのソースのfilter関数の箇所を分離するのがReduxの役目

ソース
const playernames = ['nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada'];
String.prototype.contains = function(string) { return this.indexOf(string) != -1 }

class View {
  // 動作も状態管理も状態変更後どうするかも全部ここに書かれちゃってる
  // このfilter関数が実行され終わったとき、誰もfilterNameを保持していない
  filter(filterName) {
    let playernamesForDisplay = null;
    if (filterName) {
      playernamesForDisplay = playernames.filter(
        playername => playername.contains(filterName)
      );
    } else {
      playernamesForDisplay = playernames;
    }
    console.log(playernamesForDisplay);
  }
}

let view = new View();
console.log(playernames);

view.filter('nakada');
view.filter('ma');
view.filter('');
実行結果
$ node reduxtesttest.bundle.js
[ 'nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada' ]
[ 'nakada', 'nakadayama' ]
[ 'nakadayama', 'yamada', 'hamada' ]
[ 'nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada' ]

スライドの中身を含めてReduxを使って作ったソース(要Babel, Redux)

スライド見てからの方がわかりやすいと思います。
※印に関しては、このソースの下に説明が書いてあります。

ソース
import { createStore } from 'redux';

// Helper function
String.prototype.contains = function(string) { return this.indexOf(string) != -1 }

// Action Creator
function filterPlayer(filterPlayername) {
  // Action
  return {
    type: 'FILTER_PLAYER_NAME',
    filterPlayername: filterPlayername
  };
}

// Reducer
// ※default parameters
// ※Object.assign
function playerData(state = { playernames: ['nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada'] }, action) {
  switch(action.type) {
    case 'FILTER_PLAYER_NAME':
      return Object.assign({}, state, {
        filterName: action.filterPlayername
      });
    default:
      return state;
  }
}

let store = createStore(playerData);

// View
class View {
    // filterするのと、displayPlayerNameをそれぞれ独立して書ける。
    filter(name) {
      store.dispatch(filterPlayer(name))
    }

    displayPlayerName(state) {
      // ※Destructuring
      const { playernames, filterName } = state;

      let playernamesForDisplay = null;
      if (filterName) {
        playernamesForDisplay = playernames.filter(
          playername => playername.contains(filterName)
        );
      } else {
        playernamesForDisplay = playernames;
      }
      console.log(playernamesForDisplay);
    }
}

let view = new View();

// Redux observes state and if state is changed, `view` displays PlayerName;
let currentValue;
function handleChange() {
  console.log('----- handleChange() Called -----');
  let previousValue = currentValue;
  currentValue = store.getState();

  if (previousValue !== currentValue) {
    view.displayPlayerName(currentValue);
  }
}

let unsubscribe = store.subscribe(handleChange);
// Initialize handler (as if `do {} while {}`)
handleChange();
console.log("========== INITIALIZED ===========");

view.filter("nakada");
view.filter("ma");
view.filter("");
実行結果
$ node reduxtest.bundle.js
----- handleChange() Called -----
[ 'nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada' ]
========== INITIALIZED ===========
----- handleChange() Called -----
[ 'nakada', 'nakadayama' ]
----- handleChange() Called -----
[ 'nakadayama', 'yamada', 'hamada' ]
----- handleChange() Called -----
[ 'nakada', 'tanaka', 'nakadayama', 'yamada', 'hamada' ]

ES6の説明のためのソース

ソース
console.log('----- default parameters ------');
function f(x = { a: 2 }) {
    console.log(x.a);
}


console.log('--- no arguments')
f();
console.log('--- argument one')
f({ a: 'xxx', b: 'yyy'});

console.log('----- Object assign ------');
// ※Object assign
let assigned = Object.assign({}, {p: 3}, {q: 4}, {r: 'aa', s: {}});
console.log(assigned);

console.log('----- destructuring ------');
let coodinate = {
    x: 1,
    y: 2,
}

const { x, y } = coodinate;
console.log("x's value:", x);
console.log("y's value:", y);

console.log('----- arrow function ------');

console.log('--- printXXX')
let printXXX = () => console.log('XXX');
printXXX();

console.log('--- plus3')
let plus3 = x => x + 3;
console.log(plus3(5));

console.log('--- reduce')
console.log([1, 5, 8, 10, 7].reduce((p, n) => p + n));
実行結果
$ node reduxtest-explain.bundle.js
----- default parameters ------
--- no arguments
2
--- argument one
xxx
----- Object assign ------
{ p: 3, q: 4, r: 'aa', s: {} }
----- destructuring ------
x's value: 1
y's value: 2
----- arrow function ------
--- printXXX
XXX
--- plus3
8
--- reduce
31

Reduxは何が嬉しいのか??

結構冗長にはなってしまうが、「何をするか、どうなるか、こういうときどうするか」を分離して考えることができる。

  • 「彼をトイレに移動させる」
  • 「彼はトイレに行くとおしっこを出せる状態になる」
  • 「おしっこを出せる状態になれば彼はズボンを下げる」

とそれぞれ分離できるので、状態が混ざらない。あるいは、状態が複雑に絡まっても気にしなくていい。

こういうパターンを考えないと、

  • 「彼をトイレに移動させると彼はズボンを下げる」

みたいな書き方になって、
おしっこを出す出さないに関わらずズボン下げっぱなしだったりして、デバッグが大変。

まとめ

このサンプルでは、アクションが1つ、stateも1つしかないので、イメージがつきにくいが、複数になってくると、問題が分離できてらくちん。

Functional Reactive Programmingが発想元なので、こっちを読むとイメージつきやすいかもしれない。
Reduxの中でrxjsを使っているもよう。
github.com

Reactive Programmingのおまけ( rxjs, rxjs-esが必要)

import Rx from 'rxjs/Rx';
import 'rxjs/add/operator/map';

function listen(element, eventName) {
    return Rx.Observable.create(observer => {
        // Create an event handler which sends data to the sink
        let handler = event => observer.next(event);

        // Attach the event handler
        element.addEventListener(eventName, handler, true);

        // Return a function which will cancel the event stream
        return () => {
            // Detach the event handler from the element
            element.removeEventListener(eventName, handler, true);
        };
    });
}

// Return an observable of special key down commands
function commandKeys(element) {
    let keyCommands = { "38": "up", "40": "down" };

    return listen(element, "keydown")
        .filter(event => event.keyCode in keyCommands)
        .map(event => keyCommands[event.keyCode])
}

let subscription = commandKeys(document).subscribe({
    next(val) { console.log("Received key command: " + val) },
    error(err) { console.log("Received an error: " + err) },
    complete() { console.log("Stream complete") },
});

f:id:haruharu1:20160708181323p:plain