upinetree's memo

Web系技術の話題や日常について。

redux-thunk で雑に getState するのは控えめにしたほうが良いかも

たとえばこういう Store があるとき。

const reducers = combineReducers({
  foo: fooReducer,
  bar: barReducer
});

const store = createStore(
  reducers,
  applyMiddleware(thunk)
);

こんな感じの雑さで getState() して現在の state を取っていたとする。

const fooAction = () => (dispatch, getState) => {
  const hoge = getState().foo.hoge;
  dispatch({ type: FOO, hoge });
};

ここでは combineReducers() している通り、 getState().foo.hogefoo キーを指定して書かねばならない。 なぜなら getState() は全体の state を返すので。

でもこうすると特定の reducre に依存した Action Creator になってしまう。 最初のうちは良いかもしれないけど、combineReducers を修正するとか、他の文脈でその Action Creator 使うってときに困る。 つまり Action Creator の使い回しが難しくなる。 (他の文脈で Action Creator を使いまわすというのは気をつけたほうが良いかも。必要に応じて中身のビジネスロジックだけ切り出すほうが適切なこともある。知見ほしい)

こうならないためには Action Creator の呼び出し時に引数で受け取るとよさそう。

const fooAction = hoge => (dispatch, getState) => {
  dispatch({ type: FOO, hoge })
};

// Component
const FooComponent = ({ hoge, fooAction }) => {
  const onClick = () => { fooAction(hoge) };
  return <a onClick={onClick}>Foo</a>;
);

export default connect(
  ({ foo }) => ({ hoge: foo.hoge }),
  dispatch => ({ fooAction: dispatch(fooAction) })
)(FooComponent);

getState を使うのは、どうしてもそうしないと state が取れないってときに割り切る?

どう Store を構成するかっていうのはなるべく Action Creator に入り込まないようにして、Container側で吸収したほうが良さそう。

React の Component 間通信について色々実験してみる

React さんのこといまいちよく理解していなかったので、コンポーネントのやり取りを題材に色々試してみました。 コード例は載っていますが思考実験的なのでかなり雑な点ご了承ください。

対象課題

たとえばモーダル表示切り替えを考える。 DOM構造は下記のようなイメージ。

main
  main-content
  modal-open-button
modal
  modal-content
  modal-close-button

modal は全体に覆いかぶさる要素なので、CSS的に main 要素と同列の構造で扱いたい。 (ルートスタックコンテキストを適用したい)

modal-open-button により modal が開き、 modal-close-button により modal が閉じる。 main コンポーネント(もしくは modal-open-button コンポーネント)と modal コンポーネントの間での相互作用が必要になるが、これらは親子関係にないので props の受け渡しでは単純に表現できない。

SPA (Single Page Application) ではなく、サーバサイドのテンプレートエンジンで render した結果に対してJS制御を付与する。 つまりサーバーサイドのテンプレートエンジンの実行と局所的な React コンポーネントが共存する環境を想定する。

コンポーネントを作成

大本の親(コンテナ)コンポーネントを作り、その子コンポーネントとして含めるアイデア

container
  main
    modal-open-button
  modal
    modal-close-button

container に modal の状態を持たせ、 main や modal コンポーネント内でその状態を変えるコールバックを実行し伝播する形になる。

おそらく最もポピュラーな方法だが、コンポーネントツリーが深くなってくると問題になる。 何をするにしてもルートコンポーネントを辿って反映しないといけない。

また今回の例では他の問題もある。

  • サーバサイドで render する予定だった main 内部まで React コンポーネントに含まないといけない
  • container という責務が曖昧で大きなコンポーネントが作られてしまう

これらは、SPAのような全体をReactコンポーネントを組み合わせたアプリケーションなら問題なさそう。

Flux パターン

有名なやつ。 実装としては Flux や Redux などがある。 公式ドキュメントやサンプルが異様にしっかりしているのと、情報もたくさんあるのでここでは省略。

Flux と Redux 両方試してみた感触としては、Redux を react-redux なしで使い始めるのが最初はわかりやすさの面で良いかも?と思った。 (react-redux はパフォーマンス最適化されてるので必要性が増えてきたら積極的に導入すると良さそう)

Observer パターン

Flux パターンに乗っておけばとりあえずは大丈夫そうだけど、シンプルな用途には大げさ感がある。 そういうときは最小限のエッセンスだけを真似してみても良い。

Flux は Observer パターンと、 PubSub パターンのようなメッセージパッシングを組み合わせて、単方向データフローとなるように整えた感じのやつと考えられそう。 状態を保持する Store とコンポーネントの View を分け、その間を Observer パターンで通信することだけを真似してみる。

下記がその実装例。 各 Component が Observer で、 Store が Observable (Subject) にあたる。

import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';

// Observable
class ModalStore {
  constructor() {
    this.state = { active: false };
    this.observers = [];
  }

  activate() {
    this.state.active = true;
    this.notify(this.state);
  }

  deactivate()  {
    this.state.active = false;
    this.notify(this.state);
  }

  registerObserver(observer) {
    this.observers.push(observer);
  }

  notify(state) {
    // TODO: register でコールバックを登録するように実装すると依存が減らせる
    this.observers.forEach(l => {
      l.setState({ active: state.active })
    })
  }
}

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.handleClose = this.handleClose.bind(this);
    this.state = { active: props.active }
  }

  componentDidMount() {
    this.props.store.registerObserver(this);
  }

  render() {
    const modalClass = classNames(
      'modal',
      { 'modal_active': this.state.active, }
    );

    return (
      <div>
        <div className={ modalClass }>
          <div className='modal__text'>
            <div>active: { `${this.state.active}` }</div>
          </div>
          <div className='modal__close' onClick={this.handleClose}>
            [x] close
          </div>
        </div>
      </div>
    )
  }

  handleClose(e) {
    this.props.store.deactivate();
  }

  static get defaultProps() {
    return { active: false }
  }
}

const ModalActivator = (props) => {
  const handleOpen = (e) => {
    props.store.activate();
  }

  return (
    <div className='modalFull__open' onClick={handleOpen}>
      [x] open
    </div>
  )
};

document.addEventListener('DOMContentLoaded', () => {
  const modalStore = new ModalStore();

  ReactDOM.render(
    <Modal store={modalStore} />,
    document.body.appendChild(document.createElement('div'))
  );

  ReactDOM.render(
    <ModalActivator store={modalStore} />,
    document.getElementById('main')
  );
});

EventEmitter を使った Observer パターン

Observer パターンを自前で用意するよりは EventEmitter みたいなのを使っても良い。

https://www.npmjs.com/package/wolfy87-eventemitter

import EventEmitter from 'wolfy87-eventemitter';

class ModalStore extends EventEmitter {
  constructor() {
    super();
    this.state = { active: false };
  }

  activate() {
    this.state.active = true;
    this.emitChange();
  }

  deactivate() {
    this.state.active = false;
    this.emitChange();
  }

  registerObserver(observer) {
    this.on('change', () => {
      observer.setState({ active: this.state.active });
    });
  }

  emitChange() {
    this.emit('change');
  }
}

表現力が上がったほか、手段が統制された。

なお、 registerObserver(observer) のかわりに addChangeListener(callback) のようにすると、呼び出し側への依存を減らせる。

Mediator パターン

これまでの例には Flux と違ってメッセージ配送を制御する Action や Dispatcher がないので、 Store - Component 間が複雑(多対多とか)になるとコードの治安が悪くなるかもしれない。 そのときは Mediator パターンを使ってもよさそう。というわけで実験。

class ModalStore {
  constructor() {
    this.state = { active: false };
  }

  activate() {
    this.state.active = true;
  }

  deactivate() {
    this.state.active = false;
  }
}

class ModalMediator {
  constructor(store) {
    this.stores = [store]; // 雑に多対多を想定してみる
    this.listeners = [];
  }

  activate() {
    this.stores.forEach(s => { s.activate(); });
    this.notify({ active: true });
  }

  deactivate() {
    this.stores.forEach(s => { s.deactivate(); });
    this.notify({ active: false });
  }

  register(listener) {
    this.listeners.push(listener);
  }

  notify(state) {
    this.listeners.forEach(l => {
      l.setState({ active: state.active })
    })
  }
}

class Modal extends React.Component {
  componentDidMount() {
    this.props.mediator.register(this);
  }

  handleClose(e) {
    this.props.mediator.deactivate();
  }
  
  // ...
}

const ModalActivator = (props) => {
  const handleOpen = (e) => {
    props.mediator.activate();
  }
  // ...
};

document.addEventListener('DOMContentLoaded', () => {
  const modalStore = new ModalStore();
  const modalMediator = new ModalMediator(modalStore);

  ReactDOM.render(
    <Modal mediator={modalMediator} />,
    document.body.appendChild(document.createElement('div'))
  );

  ReactDOM.render(
    <ModalActivator mediator={modalMediator} />,
    document.getElementById('main')
  );
});

うーん、この例ではあまり必要性が実感できない…。 非同期処理とか複数 Store の順序依存とかが出てくるとメリットがわかりやすそう。 そうなる前に Flux, Redux に移れという話になるのかな…。

感想

上記はいづれも粗い実装ではありますが、最低限PDSは守られているので Flux に乗せるのも簡単のはずです。 というか複雑性に対処しようとしていろいろやっていくと Flux に近づいていく気がします。

では最初から Flux でいいのかというとそうでもなくて、状況(複雑性、リソース、スケジュール、コードの寿命)に合わせて引き出しをいくつか持っておくと良さそうです。

そしていろいろ試した結果、こういう一部だけコンポーネントを使うようなケースには、 React じゃなくて Vue 等のほうが向いているな、と思ったのでした…。 次は Vuex を触ってみよう。

参考

明日から使えるテスト技法をざっくり社内で紹介した

弊社では月一で帰社日という全社員が集まるイベントがあります。

そこで有志が発表する枠があるのですが、そのなかでも「基調講演」と銘打ってまあちょっとだけ気合い入れて発表するコーナーがあるのですね。それが今月は僕の番ということで、テスト技法についてざっくり発表してきました。 以下がそのときの資料です。

speakerdeck.com

…正直ざっくりとはいえ結構ボリュームあって規定の時間を大幅にオーバーして喋ってた気がします。

テスト技法と一言に言ってもかなり広くて奥が深い世界で、どこをどう説明しようか悩みました。主に仕事で使いやすいところを選んでみたつもりです。一見すると知ってるよ、という知識かもしれませんがちょっと踏み込むと新しいことが知れてたのしいです。

なんで今テスト技法なの、と思われたかもしれません。 テストってなんか地味なイメージあるけどやっぱりソフトウェアと切っては切れない関係なわけで、どうせテストするならしっかり基礎は押さえておきたいと思って、最近勉強していたのです。 鉄は熱いうちにということで、こうして発表ネタにしたというわけですね。そのかいあって自分自身学んだ知識や不明点が整理できて良かったです。

リソースの制約があってテストは手を抜かざるを得ない場面というのに出会うこともある思うのですが、手を抜くにしても分かった上でやるのとそうでないのとでは違いますよね。どう手を抜くの、どこにリスクを置いてどうカバーするのみたいなことをきちんと考えていきたいものだなーと思っています。

なお

弊社では一緒に働いてくれる仲間を募集中です!

www.wantedly.com

native extension に openssl を使う gem のビルドに失敗するときの対処法

homebrew で openssl をインストールした場合、今までだとbrew linkするだけで特に問題なく使えていたが、最近ではbrew linkができなくなった。

$ brew link openssl --force
Warning: Refusing to link: openssl
Linking keg-only openssl means you may end up linking against the insecure,
deprecated system OpenSSL while using the headers from Homebrew's openssl.
Instead, pass the full include/library paths to your compiler e.g.:
  -I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib

その結果、 native extension のビルドに openssl を使う gem が正しく include できなくなった。 なので次のように設定した。 (参考: rbenv/ruby-build: Compile and install Ruby

$ cat ~/.zshrc
if [ $(uname) = "Darwin" ]; then
  export CONFIGURE_OPTS="--with-opt-dir=`brew --prefix openssl`"
  export RUBY_CONFIGURE_OPTS="--with-opt-dir=`brew --prefix openssl`"
fi

$ source ~/.zshrc

それでruby再ビルドしたらいけた。

Rails 5.0.1 の変更点で個人的に気になったところ雑まとめ

先日12/21に Rails 5.0.1 が出ましたね。 CHANGELOG をざっと読みながら、個人的に気になったところを雑にまとめます。 雑まとめなので間違ってるかもしれません。正確な情報はリンク先を見てください〜。

3行で

ActionCable

https://github.com/rails/rails/blob/v5.0.1/actioncable/CHANGELOG.md

ActionPack

https://github.com/rails/rails/blob/v5.0.1/actionpack/CHANGELOG.md

  • Add ActionController::Parameters#merge!, which behaves the same as Hash#merge!.
  • Added ActionController::Parameters#deep_dup which actually creates a params copy, instead of refereing to old references in params.
  • Add to_param to ActionController::Parameters deprecations.
    • AC::Parameters がHash継承しなくなった影響でto_paramsが従来の期待通りに動かなくなった(AC::Parametersを返す)
    • to_h してから to_param 使ってくれとのこと
    • https://github.com/rails/rails/pull/26328
  • SSL: Changes redirect behavior for all non-GET and non-HEAD requests
    • force-sslしてるときにPOSTとかするとGETでリダイレクトされてたど、それだと別のアクションを呼び出すことになるのでPOSTでリダイレクトするよ
    • そうしたときにステータスコードも適切なものに変えたよ
  • Deprecated omitting the route path. Specify the path with a String or a Symbol instead.
  • Added new ActionDispatch::DebugLocks middleware that can be used to diagnose deadlocks in the autoload interlock

ActionView

https://github.com/rails/rails/blob/v5.0.1/actionview/CHANGELOG.md

  • Render now accepts any keys for locals, including reserved words
    • renderに予約語もlocalとして渡せるようになった。local_assigns[:class]のようにして参照する
  • Changed partial rendering with a collection to allow collections which implement to_a.
    • Collection Rendering として Enumerable ならなんでも渡せるようになった

ActiveJob

https://github.com/rails/rails/blob/v5.0.1/activejob/CHANGELOG.md

  • Added instance variable @queue to JobWrapper.
    • rescue-scheduler への対応だけど、もしかしたらsidekiqとかでも便利になる?

ActiveRecord

https://github.com/rails/rails/blob/v5.0.1/activerecord/CHANGELOG.md

  • バグフィックスたくさん。特に複数スレッドでうまく動くようにしてるのが多い(クエリキャッシュとかコネクションプーリングとか)
  • Introduce Model#reload_ to bring back the behavior of Article.category(true) where category is a singular association.
    • article.reload_categoryのようにして特定のhas_one関連だけリロードできるようになった
    • 今までhas_one関連については article.category(true)でリロードできてたけど、それがRails5でdeprecatedになっていた。article.reload.categoryでは完全な代替にはならない(未保存の他の関連インスタンスの変更が消えるとか)という背景があった
    • https://github.com/rails/rails/pull/27133
  • Always store errors details information with symbols.
    • autosaveでバリデーションエラーが発生したときに保存するためのkeyをSymbolにした
    • いままではStringだったけどそれだとエラーを保存する他の処理でSymbolにしているのでずれてて扱いづらい
    • 場合によっては breaking change になるかもだけどほとんどのケースは大丈夫かな
    • https://github.com/rails/rails/pull/26552
  • Avoid loading records from database when they are already loaded using the pluck method on a collection.
    • 地味に嬉しい。reload考慮する必要ありそう?
  • Doing count on relations that contain LEFT OUTER JOIN Arel node no longer force a DISTINCT. This solves issues when using count after a left_joins.
    • むしろいままでDISTINCTだったのか

ActiveSupport

https://github.com/rails/rails/blob/v5.0.1/activesupport/CHANGELOG.md

  • サマータイム系のバグフィックスが多い
  • Fix thread_mattr_accessor subclass no longer overwrites parent.
    • 親と子で別々に属性持てるようになった

Railtile

https://github.com/rails/rails/blob/v5.0.1/railties/CHANGELOG.md

  • バグフィックスだけ

docker diff が勉強に便利

たとえば

% docker run -it --name hoge ubuntu /bin/bash
root@db1da9fc72e9:/# useradd hoge
root@db1da9fc72e9:/# exit
% docker diff hoge
C /etc
C /etc/group
A /etc/group-
C /etc/gshadow
A /etc/gshadow-
C /etc/passwd
A /etc/passwd-
C /etc/shadow
A /etc/shadow-
C /etc/subgid
A /etc/subgid-
C /etc/subuid
A /etc/subuid-
C /root
A /root/.bash_history
C /var
C /var/log
C /var/log/faillog
C /var/log/lastlog
% docker start -i hoge
root@db1da9fc72e9:/# diff /etc/passwd{,-}
...

みたいな感じにコマンドがどういう変更を行うのかを追いやすい。

ちなみに先頭のアルファベットの意味は下記の通り。

  • A - Add
  • D - Delete
  • C - Change

差分も表示してくれると嬉しいがそういうオプションはなさそう。 そういうときはdocker exportしてtarballを吐いて、コンテナ間の差分を見れば良いのかな。

Dockerコンテナに対してAnsibleで雑にプロビジョニングする

Packer の ansible provisioner とか使っていると、変更差分の動作確認したくなってくる。 毎回 packer build するのは時間がかかり休憩が捗りすぎて進捗がでないという問題点があり、なんとかしたい。

しかしそのために環境作るのも面倒だ。そこで前回出力した docker image を元にコンテナ立ち上げてそこに ansible 走らせたらええやんと思いつく。 といってもsshd立てるのも面倒だし微妙らしいので方法を探していたら、どうやら Docker Connection Plugin というものが標準装備されているらしい。

$ docker-compose run --rm -u root web bash
$ ansible-playbook -i web_run_1, -c docker ansible/web.yml
  • -i (--inventory)のhostsはコンテナ名
  • -c (--connection)dockerを指定

Pakcer の ansible provisioner が用意する ansible とローカルの ansible のバージョンが違うことがあるのでそれだけ注意。

参考