いっきのblog

技術とか色々

LaravelのバッチをDiddagで運用したら少し幸せになった話

どうも、くずきです。
弊社の本番環境のバッチはスピード優先で作ったのもあって、処理ごとに分けてはいるものの連携を取ろうとすると以下のことをしなきゃいけない。

  1. 手動で1つ目のバッチを回す
  2. 1つ目が終わったら次のを回す
  3. それが終わったら次を回す・・・

コマンド一つ叩くだけなので、大きな作業は発生しないにしろバッチとバッチの間の無駄な時間やどこまでバッチが終わって、どれくらいかかって、こけた場合はどこでこけたのか・・など色々問題があった。

そこで、Laravelのタスクスケジュール使う手もあったんだけどフローの可視化やリトライ処理を簡単にやりたいと思っていたので、前から良さげだなとおもっていたDigdagを導入してみました。

Digdagとは

あらゆる手動作業を自動化しよう!!というコンセプトで作られたワークフローエンジンです。

github.com

細かい説明は、作成者でもある古橋さんの資料を参考にしましょう。。。

www.slideshare.net

とりあえずいいところとしては、

  1. ymlっぽいやつで簡単にかける
  2. 高可用性な構成にできる
  3. 分散処理できる
  4. テスト段階ながら、UI(可視化など)が提供されている

かなと思う。

既存バッチの把握

まずは、既存バッチを把握する。
Step1 ~ Step4までバッチが分かれており、簡単な図を混ぜながら説明。

  • Step1: 処理したいデータをキューに入れる  
    • sshでログインしてバッチを叩く
    • 3時間前後で終わる

f:id:kzkohashi:20171203230211p:plain

  • Step2: キューに入ってるデータを処理する
    • バックグラウンドで常に動いてるため、特にすることはない
    • 外部のデータに依存するので、長いと数日かかる

f:id:kzkohashi:20171203230250p:plain

  • Step3&4: sshでログイン後Step2のデータを処理する
    • sshでログインしてバッチを叩く
    • 8時間前後で終わる

f:id:kzkohashi:20171203230258p:plain

Digdagへの移行

Digdagのインストールについては今回は省く。
ここに書いてある通りやればすぐ使えると思う。

Getting started — Digdag 0.9.5 documentation

一応載せとく。

$ curl -o ~/bin/digdag --create-dirs -L "https://dl.digdag.io/digdag-latest"
$ chmod +x ~/bin/digdag
$ echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc

ちなみに、batch server 1 にインストールしてある。

次にPHPの起動については特に用意はされていないので>shコマンドを使う。

sh>: Shell scripts — Digdag 0.9.5 documentation

このままだとStep2の処理はバックグラウンドで動いてるため、Step3へのフックがない。
そこで、Step2が終わっているかの処理(キューにデータがあるかなど)を行うバッチを追加で書いた。

Step2_task_check

    public function handle()
    {

        $this->line(Carbon::now() . ': start step2_task_check');

        for(;;) {
        
          if (キューの中身があるか)
          {
            // 0を返す
            return config('command.exit_code.SUCCESS');
          }
          sleep(60);
        }


        $this->line(Carbon::now() . ': end step2_task_check');
        
        // 1を返す
        return config('command.exit_code.ERROR');

    }

0 or 1を返すようにすれば、Digdag側で成功か否かを判断してくれる。
ここまで準備したらあとはymlっぽいやつをかけば終わり。

BatchFlow

timezone: UTC

+setup:
  echo>: start ${session_time}

+disp_current_date:
  echo>: ${moment(session_time).utc().format('YYYY-MM-DD HH:mm:ss Z')}

+step1:
  sh>: php /var/www/app/artisan app:step1

+step2:
  sh>: php /var/www/app/artisan app:step2_check

+step3:
  sh>: php /var/www/app/artisan app:step3

+step4:
  sh>: php /var/www/app/artisan app:step4

+teardown:
  echo>: finish ${session_time}

ほぼほぼチュートリアルのままで完成。
特にエラー処理とかDigdagの売りなことは特にしてないんだけど、まずは少し幸せになるという目標だったので自分的にはこれだけでもかなり楽になったし、細かくバッチを作れば組み合わせも楽なので嬉しい。

Digdagに処理をpushする

batch server 1Digdag Serverは起動してある。

digdag push batch_flow

こんな風にpushする。

f:id:kzkohashi:20171203230329p:plain

Digdag UI で起動&確認

Digdag UIDigdagを起動したら勝手に立ち上がっている。
ただ、外向けのポート開けてないっぽいため65432sshのトンネル作って確認する。

ssh -L 8000:localhost:65432 ec2-user@ip -i "~/.ssh/xxx.pem"

その後、pushされた処理があるため、それを起動すれば完了。

終わりに

Digdagの色々良いところは使いきれてないけど、「手動を自動化する」というミッションに関してはすごく簡単にできたので良いと思います。
次くらいまでには、障害に強い構成にしたり、バッチのリラン、Slackへの通知などやる予定。  

tmux + ssh + peco + direnvで複数のAWSのサーバーへログインするやり方

どうも、くずきです。
今回は自分がいつも使っているスクリプトについて紹介したいと思います。
(色々な方のスクリプトをパクってくっつけてるだけですw)

tmuxの導入

github.com

tmuxとは端末を多重化するツール(ソフト)。
これだけでも色々かけるんだけど、色々な人がやってくれてるからグーグルで最上位にきてた記事だけ貼っとく。

qiita.com

tmux + sshで複数ホストへの同時接続

tmuxではサーバーの複数のホストへ同時に接続できる。
完成形録画したかったのだが、、ちょっとうまく撮れなかったので参考サイトでイメージつかんで欲しい。

blog.yuuk.io

また、この記事にあるtsshというtmuxで簡単に同時接続できるスクリプトがあるのでそちらを使う。記事からスクリプトのリンクが切れてるため一応貼っとく。
(ブログ兼スクリプトの作者様ありがとうございます・・・)

github.com

tsshコマンドとして使いたいため、/usr/local/bin/tsshに中身をコピーしておく。
ただ、今回の使いかだと少し改造する必要があるため、一部変更させてもらう。

# cmd="ssh $SSH_OPTION $host" 
cmd="ssh $SSH_OPTION $USER@$host" # $USERを追加

pecoの導入

github.com

pecoとはコンソールから標準入力を検索し、その検索結果を標準出力として返すことができる検索ツールみたいなもんである。
ここらへんもたくさん導入方法あるから頑張って入れよう。

AWS CLIの導入

AWS コマンドラインインターフェイス(CLI - AWS サービスの制御・管理)|AWS

コンソールからAWSにログインするためには、公式が用意しているCLIを入れる。
また、AWSからのJSONレスポンスを扱いやすくするために、jqと言われるJSONの扱いに長けたツールもいれとく。

https://stedolan.github.io/jq/

tssh + peco + aws cli(+jq)を使ったログイン

/usr/local/bin/floginに以下のコードをコピペしよう。
ちなみになんでfloginかというと、ファイナルログインの略だ。

iplist=$(aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" "Name=tag:Name,Values=*" | jq -r '.Reservations[].Instances[] | {InstanceName: (.Tags[] | select(.Key=="Name").Value), PublicIpAddress} | "\(.InstanceName) \(.PublicIpAddress)"' | sort | peco | cut -d " " -f 2)
tssh $iplist

何をしているかというと、AWS CLIrunning中のEC2情報をとってきて、jqでホスト名 + IP に整形している。
その後pecoに渡すことで検索(複数選択)を可能とし、そこから標準出力でIPのリストをスペース区切りで出力し、tsshに渡している。

これだけでも同時ログインができて便利だが

# aws cliで使う
export AWS_ACCESS_KEY_ID=access_id
export AWS_SECRET_ACCESS_KEY=secret_key

# tsshでのsshログイン時に使う
export SSH_OPTION="-i ~/.ssh/xxx.pem"
export USER=ec2-user

環境ごとにこれらを設定しなくてはいけない。

direnvで環境ごとにexport

github.com

direnvディレクト単位で変数を設定できる。マジ便利。
ディレクトリの中で

direnv edit .

で環境ごとにexport設定をしてあげればよい。
自分の使い方は/会社/サービス名/環境(本番など)で区切っている。
設定が終わったら、あとは入りたい環境にcdして、floginを実行すればいい。

終わりに

だいぶインストールはしょったけど、個人や自分の会社、他社さんのサービスを同時に管理している時はほんと助かってる。
改善点としては、AWSのログイン時に指定したサービスしかそもそもでないようにしたいなぁと。そしたらpecoもいらなくなるか・・・。(それはそれで使いづらいかもだけど・・)

Reactでテーブルをソートするやり方

どうも、くずきです。
Reactでテーブルをソートしようとした際に、View自体のソートはあるんですが
サーバーと連動したやつがなかったので実装してみました。

やりたいこと

f:id:kzkohashi:20171119182707p:plain

上記の画像はすでに完成品だけど、

  • の部分のようなソートボタンを作る
  • ソート中のカラムはどれなのか色をつける
  • ソートが降順か昇順かわかるようにする
  • ソートした際URLが変わるようにする。

が、できれば問題ない。
ソートした場合にデータ取りに行く必要はあるんだけど、以前紹介したURLが変更された際にリソースと再取得するという処理を親のコンポーネントに仕込んでいるので、URLさえ変更すればいい。

kzkohashi.hatenablog.com

コンポーネントの準備

まずは、テーブル用のコンポーネントの準備。
色々省略して、render部分あたりのみ。


render() {
  return(
    <table>
      <thead>
        <tr>
          <th className="prof-image"></th>
          <th className="name">
            <p className="text-left">
              <Link to={TableUtil.getSortUrl("name")} //このカラムがソートされるURL
                    className={TableUtil.getSortClass("name")} //このカラムがソートされている場合のクラス
              >
                <span>名前</span>
              </Link>
            </p>
          </th>
        </tr>
      </thead>
      <tbody>
        {tableContent} // ソートされるごとに変わるtbodyの中身
      </tbody>
    </table>
  );
}

重要なのは、Link部分が - 自身がソートされるURLを持つ - 自身がソートされてる場合デザインを変更するためのクラスの付与 を行う設計する(よくありがちなやつ)。

クエリパラーメーターを連想配列にする

上記のことを行うために、クエリパラメーターを確認しどのカラムがソートされていて、昇順/降順なのか確認する必要がある。
そのために、まずはクエリパラーメーターを扱いやすいように連想配列にする。

Query.js

export function getQueryParam() {
  let vars = {}, max = 0, hash = "", array = "";
  let url = window.location.search;

  hash = url.slice(1).split("&");
  max = hash.length;
  for (let i = 0; i < max; i++) {
    array = hash[i].split("=");
    if (array[1] !== undefined) {
      vars[array[0]] = decodeURI(array[1]);
    }

  }

  return vars;
}

書き方は突っ込まずに、とりあえず格納した連想配列を返している。

自身がソートされるURLを持つ

ソートされるカラムをsc、昇順降順をsdとする。

Table.js

export function getSortUrl(sortColumn) {
  let sortUrl = `${window.location.pathname}?`;
  let query = Query.getQueryParam();

  // クエリパラメーターにscがなかった場合、受け取ったカラムでパラメーターを作る(デフォルト降順[desc])
  if (!query["sc"]) {
    sortUrl += `sc=${sortColumn}&`;
    sortUrl += `sd=desc&`
  }

  for(let key in query) {
  
    // pagenationは除外
    if (key === "page") {
      continue
    }
    
    // scが存在する且つ受け取ったカラムではない場合、ソートするカラムを上書き
    if(key === "sc" && query["sc"] !== sortColumn) {
      sortUrl += `${key}=${sortColumn}&`;
      continue;
    }
    
    // sdが存在する且つscが受け取ったカラムではない場合、降順にする
    if(key === "sd" && query["sc"] !== sortColumn) {
      sortUrl += `${key}=desc&`;
      continue;
    }

    // sdが存在する且つscが受け取ったカラムの場合、ソートの順番を逆にする
    if(key === "sd" && query["sc"] === sortColumn) {
      let sdCol = (query["sd"] === "desc")? "asc" : "desc";
      sortUrl += `${key}=${sdCol}&`;
      continue;
    }

    // その他色々付いているクエリパラーメーターはそのままURLに含める
    sortUrl += `${key}=${query[key]}&`;
  }

  return sortUrl.slice(0, -1);
}

だいぶゴリゴリゴリラな書き方だけど、最適化はそのうち・・・。
自身のカラムと比較してなかったらソートするカラムを追加、ある場合はソートを逆にするなど行なっている。

ソートのデザインを変更するためのクラスの付与

自身がソート対象だった場合、activeを付与してデザインを変更する。

Table.js

export function getSortClass(sortColumn) {
  let className = "sort";
  let query = Query.getQueryParam();
  if (query["sc"] === sortColumn) {
    className = `${className} active ${query["sd"]}`;
  }

  return className;
}

CSS部分

Saasだけと貼っておく。

.sort {
  position: relative;
  display: inline-block;
  padding-right: 14px;
  &:before {
    position: absolute;
    top: 20%;
    right: 0;
    width: 5px;
    height: 5px;
    border-right: 1px solid gray;
    border-bottom: 1px solid gray;
    content: '';
    -webkit-transform: rotate(45deg);
    -ms-transform: rotate(45deg);
    transform: rotate(45deg);
  }

  &.active {
    &:before {
      border-color: red
    }
  }

  &.asc {
    &:before {
      top: 5px;
      -webkit-transform: rotate(225deg);
      -ms-transform: rotate(225deg);
      transform: rotate(225deg);
    }
  }
}

終わりに

ゴリゴリゴリラだからもうちょっとクールに直したい。

今年はRedashのあどべんとかれんだーやることなりました。

qiita.com

HighCharts(React)でグラフをHoverした際に画像を表示する

どうも、くずきです。
以前HighChartsと呼ばれる、JSでのグラフ系ライブラリでかなり人気っぽさそうなやつを使った。
綺麗だし、簡単だしと良いところ盛りだくさんなライブラリだけど、商用利用の場合は有料のため注意。

Reactで利用するために以下のライブラリを利用。

github.com

ReactHighstockなどがうまくインポートできなかったりする難点があるけど・・。

今回利用するバージョン

ライブラリ バージョン
react 15.6.1
react-highcharts 13.0.0

tooltipでHover時に画像を表示する

HighChartsはところどころでカスタマイズできるので良い。 Hover時の挙動はtooltipの項目で変更可能です。

tooltip: {
  useHTML: true,
  formatter: function() {
    if(this.series.type === 'line') {
      return false;
    }

    return '<img src="' + this.series.userOptions.thumbnails[this.point.x]  + '" width="200" height="200" />';
  }
}

何をしているかというと、棒(column)と折れ線(line)の複合グラフを想定していて、
折れ線グラフの際はfalseを返すことで何もしません。

それ以外は、seriesあるthumbnailsという配列の中身を表示させてます。(後述)
thumnailsじゃなくてもカスタムの名前でOK。

表示された画像をクリックすると遷移させる

厳密に言うと、グラフ自体をクリックしても遷移する。
こちらも、URLsというカスタム項目の中に配列でいれておけばよい。

series: [{
  type: 'column',
  name: '棒グラフ',
  yAxis: 1,
  point: {
    events: {
      click: function() {
        var url = this.series.userOptions.URLs[this.x];
        if (url)
          window.open('http://'+url);
      }
    }
  },
  URLs: ['url_1', 'url_2'...],
  thumbnails: ['thumbnails_url_1','thumbnails_url_2'...]
}, {
  type: 'line',
  name: '折れ線グラフ',
}
]

eventsclick項目に関数をしていすれば簡単に実装できる。

参考になりそうなサイト

grgrjnjn.blogspot.jp

qiita.com

Laravel + Reactで画像をs3にアップロードする

どうも、くずきです。
今回使用するバージョン

ライブラリ バージョン
react 15.6.1
Laravel 5.5.19
league/flysystem-aws-s3-v3 1.0.18

React周りはcreate-react-appのバージョン1.4.0を使って作成している。

kzkohashi.hatenablog.com

Laravelでs3にアップロードする準備

Laravelfilesytem機能をleague/flysystem-aws-s3-v3を使って簡単に拡張できる。 公式の説明がわかりやすくて、そちらを参考にした方が良いです。

ファイルストレージ 5.5 Laravel

コードだけのせておく

// ControllerでAPIの準備しておく

public function upload(Request $request)
{
    $item_image_path = "";
    if ($request->hasFile('item_image_file')) {
        $item_image_path = $request->item_image_file->store('images/item', 's3');
    }

    return response()->json(["href" =>"//".env('CDN_DOMAIN')."/" . $item_image_path]);
}

Reactでファイルをアップロードする

自分はここで少しはまった。
Javascriptではアップロード時に取得したファイルデータをそのままアップロードしても、multipart/form-data 扱いにならない。

そこで、FormDataモジュールを使って変換してあげる必要がある。

developer.mozilla.org

受け取ったデータ(targetfilesの中)をFormDataappend関数にいれてあげるだけでよい。

const formData = new FormData();
formData.append('item_image_file', e.target.files[0]);

UploadAction.uploadItemImage(formData);

uploadStore.on("item_image", (v) => {
  this.props.onChange(v.href);
});

今回はアップロード後にURLが返ってくるので、それを表示している。

終わりに

わかってしまえば簡単だった。

laravel-adminでパスワードの設定の仕方

どうも、くずきです。
こないだLaravel + ReactWebサービスを構築した際に、社内の人間が使う管理画面も作ることになりました。
社内の人が使う管理画面なんて絶対に工数かけたくないので、調べた結果

qiita.com

の記事で試していた、

github.com

を使ってみた。結論から言うと神ってるほど楽。それはまた別記事で・・。

今回はlarave-adminでパスワードを扱う場合のやり方について書こうと思う。

パスワードの隠し方

larave-adminドキュメントがある程度そろっており、passwordの設定方法も記載されている。

http://laravel-admin.org/docs/#/en/model-form-fields?id=password-input

ただ、これだと若干わかりづらく、誤って$form->password('password')と設定するとパスワードが丸見えという状態になってしまう。
そこで、第二引数に

$form->password('password', trans('admin.password'));

trans('admin.password')をつけることによって、パスワードが隠れる状態にできる。

確認用パスワードの設定

大体のサービスでは、パスワード設定時には確認用に入力することがあると思う。
laravel-adminでも簡単に設定でき、先に結論を書いてしまうと

$form->password('password', trans('admin.password'))
  ->rules('required|confirmed')
  ->default(function ($form) {
    return $form->model()->password;
  });
  
$form->password('password_confirmation', trans('admin.password_confirmation'))
  ->rules('required')
  ->default(function ($form) {
    return $form->model()->password;
  });

Laravelにすでにあるバリデーションと同じように、rulesconfirmedの設定と
設定した入力名 + _confirmationの入力を用意すれば実装できる。

パスワードの暗号方法と前のパスワードとの比較

パスワードも大体のサービス(というかほぼ全部)では暗号化されていると思う。
じゃあどうやって、パスワードの暗号化と前のパスワードとの比較を行うかというと

$form->saving(function (Form $form) {
  if ($form->password && $form->model()->password != $form->password) {
    $form->password = bcrypt($form->password);
  }
});

saving関数は保存する直前の処理を書くことができ、これを利用する。
保存直前に入力パスワードの比較し、前回と異なった場合はbcryptで暗号化し、入力値を上書きする。

ここまでの方法全てのコードをまとめる。

$form->password('password', trans('admin.password'))
  ->rules('required|confirmed')
  ->default(function ($form) {
    return $form->model()->password;
  });
  
$form->password('password_confirmation', trans('admin.password_confirmation'))
  ->rules('required')
  ->default(function ($form) {
    return $form->model()->password;
  });
  
$form->ignore(['password_confirmation']);

$form->saving(function (Form $form) {
  if ($form->password && $form->model()->password != $form->password) {
    $form->password = bcrypt($form->password);
  }
});

終わりに

やってみると簡単だったが、ドキュメントだけだとわかりづらいかも。

react-jsonschema-formで外部バリデーションを使用する方法

どうも、くずきです。
こないだReactでフォームを作る際にreact-jsonschema-formと呼ばれる便利なライブラリを使ったんですが、外部からのバリデーション入れるのに詰まったのでその解決法を書きたいとお見ます。

react-jsonschema-formとは

github.com

Reactでフォーム作る際に、JSONで設定ファイルを書いてしまえば自動的に作ってくれるという便利なものです。
Githubのスターが2300(2017/10月時)もあり、色々なところで使われているライブラリです。
簡単に作れるのでお試しで作りたいとか、デザイナーいないからそれぽいの作りたい時に重宝されるみたい。
自分的にはこのライブラリは独自のフォームの作成にも対応していて、インターフェースを統一しながら自分のプロダクトに合わせたものが作れるのが強みなのではないかと思う。
(近々カスタムフォーム周りの説明は記事にします)

バリデーションについて

このライブラリではバリデーションも簡単で、JSONにバリデーションの内容を書くだけでエラー文言を出してくれます。
また、カスタムのバリデーションに関しても、

function validate(formData, errors) {
  if (formData.pass1 !== formData.pass2) {
    errors.pass2.addError("Passwords don't match");
  }
  return errors;
}

const schema = {
  type: "object",
  properties: {
    pass1: {type: "string", minLength: 3},
    pass2: {type: "string", minLength: 3},
  }
};

render((
  <Form schema={schema}
        validate={validate} />
), document.getElementById("app"));

このように、pass1pass2にはJSONで長さのバリデーションをつけ、validate関数によってカスタムのバリデーションを追加できます。

しかしながら、弊社のプロダクトではアカウントの登録時にemailの重複やサーバー側とフロント側でバリデーションを揃えたいという要望があり、調べた限りですと対応していないようでした。

Storeを利用した外部バリデーション

上記の問題を解決するために、すべてのFormをカスタムフォームにし、

Form(error用のStoreに登録) 
-> Action(サーバーからのレスポンス) 
-> Store(エラー登録) 
-> Form(Storeからエラーを受け取ったら表示)

のような流れで実装しました。

Formコンポーネント

class SelectField extends Component {

  constructor(props) {
    super(props);
    this.state = {
      formData: props.formData?props.formData : null,
      errors: null
    };

    this.handleError = this.handleError.bind(this);
  }


  componentWillReceiveProps(props) {
    this.setState({
      formData: props.formData
    });
  }

  componentWillMount() {
    errorStore.addValidListener(this.handleError);
  }

  componentWillUnmount() {
    errorStore.removeValidListener(this.handleError);
  }


  handleError(_errors) {
    let errors = Validation.getValidationErrors(_errors, this.props.name);
    this.setState({
      errors: errors
    });
  }

  render() {
    return (
      ...
    );
  }
}

特段珍しいことはせず、フォームごとにerrorStoreに登録。
バリデーションの中身はすべてきてしまうため、そのフォームのエラーがあるかgetValidationErrorsで検索。
一応getValidatonErrors

export function getValidationErrors(errors, name) {
  let res = [];
  _.map(errors, (v,k) => {
    if (k === name) {
      _.map(v, (v2, k2) => {
        res.push(v2);
      });
    }
  });

  return res;
}

Action

export function itemEdit(data, id) {

  let url =  API.Item.Edit.replace(":id", id);
  request
  .post(url)
    .send(data)
    .use(AuthUtil.bearer)
    .end(function(err, res){
      if (err === null) {
        let body = res.body.success;
        dispatcher.dispatch({type:"ITEM_EDIT", body});
      } else {
        dispatcher.dispatch({type:"ERROR", res}); // レスポンスがエラー時はErrorStoreに投げる
      }
    });
}

エラーだった際にErrorStoreに投げる。

ErrorStore

class ErrorStore extends EventEmitter {
  handleActions(action) {
    if(action.type === "ERROR"){
      // エラー通知
      if(action.res.hasOwnProperty("statusCode")) {
        switch (action.res.statusCode) {
          case 500:
            break;
          case 400:
            this.emit(EVENT.ERROR.VALIDATION, action.res.body.errors);
            break;
          case 401:
            this.emit(EVENT.ERROR.AUTHENTICATION, action.res);
            break;
          default:
            break;
        }
      }
    }
  }

  addValidListener(listener) {
    this.on(EVENT.ERROR.VALIDATION, listener);
  }

  removeValidListener(listener) {
    this.removeListener(EVENT.ERROR.VALIDATION, listener);
  }

}

const errorStore = new ErrorStore();
dispatcher.register(errorStore.handleActions.bind(errorStore));

export default errorStore;

ErrorStoreではステータスコードによって通知先(emit)を変えてるので、いろいろなエラーに対応できる。(今の所)

問題点

このやり方だと問題があり、フォームごとにStoreに登録するためEventEmitter.defaultMaxListenersの上限を変えないと怒られてしまいます。
そもそも登録しすぎるとメモリ食うのでフォームを登録している親コンポーネントErrorStoreに登録し、子のフォームに伝えるやり方がパフォーマンス的には良いのかなと思ってますが・・・なるべく独立して使いやすくしたい。。

おまけ

github.com

ここで議論されてるっぽい。(Redux-formだとそれっぽいこと簡単にできそう・・・・)