BackstopJS を利用した Visual Regression Testing の感想と工夫ポイント
Visual Regression Testing とは
CSSやコードの変更によって、デザインに意図せぬ影響・リグレッションがないかを視覚的に確認するためのテスト。 変更前後のスクリーンショットを撮影して、差分を出力する方法が一般的。
似たテストに Snapshot Testing があるが、これはHTMLやASTを比較するテストであるため、CSSを含めたデザインの確認はできない。
やってみた感想
CSSのリファクタリングでは、見た目が変わっていないことや、見た目の変更が意図通りであることを確認する工程が多くを占める。 変更が大量にある場合はすべてをチェックすることは現実的ではないため、ある程度サンプリングして確認することになる。従って確認漏れの可能性は残る。 さらに、目視での確認は不確実でもあるし、何度も行っていると疲れて精度が落ちてくる(あと、とにかくつらい… :sob: )。
テストがあると、これらの作業を効率化したうえで、安心してリファクタリングできるようになる。 具体的には、次のシチュエーションで効果を実感できた。
- 全体のレイアウトに広く影響があるような修正を行うとき
- 新しく実装する機能のマークアップを行うとき
- 副次的に、レビュー依頼など変更を説明するとき
- レポート画面をスクショして提示するだけで、before/after+差分画像で変更を説明できて楽かつわかりやすい
プログラミングの自動テストと同じ感覚で、効率化と安心感をもたらしてくれた。
逆に微妙な点は、
- 実行時間が長い
- アプリケーションや環境にもよるが、100画面超えてくると10分超かかる
- 並列実行数を増やすとテスト結果が不安定になるので高速化にも限界がある
- 流して放置しつつ休憩 or 作業することができるので、目視時代よりは格段に楽
- テスト結果が不安定
- 今のところ開発環境で立ち上げたサーバでテストしているので、データの状態や実行時間などの影響が大きい
- スクリーンショットを撮影するタイミングで結果が変わることがある
- 上記は工夫次第で解決できそうだが、そのための労力が必要
BackstopJS
今回は、BackstopJSを使って実施した。 BackstopJSはスクリーンショット撮影、比較、比較結果のビジュアライズ、レポートUIといった、テストに必要な機能を十分満たしていて、かつメンテナンスも活発であることから採用した。 最近の開発シーンだとStorybookなどと組み合わせて行うのがモダンなのだろうけど、今回はそこまでJSを多用するプロジェクトではないのと、コンポーネントを組み合わせたときのレイアウトも確認したいため、どんな画面でもテストできることを重視した。
BackstopJSでは、例えばテストに失敗すると、以下のように差分が確認できる。
before/after の目視での比較も可能。
詳細はBackstopJSのREADMEを確認されたし。
利用にあたっては、標準の状態では使い勝手の良くない部分があったので色々工夫した。
BackstopJSはブラウザの操作に使うスクリプトを ./backstop_data
下に出力するので、そこをカスタマイズすることで大体解決できる。
そのカスタマイズ結果を他のプロジェクトでも使えるように汎用化して、雛形として公開してみた。
https://github.com/upinetree/backstop_boilerplate
大体コードを眺めればわかるのだが、せっかくなので以下では工夫したところやハマったところを解説してみる。
工夫ポイント
複数クリックイベント
オプション clickSelector
はクリックするセレクタを1つしか指定できないが、あるセレクタをクリックして、次にこのセレクタをクリック、ということをしたい場面は結構ある。
そのため複数のセレクタを連続でクリックする clickSelectors
を指定可能にした。
// backstop_data/engine_scripts/chromy/clickAndHoverHelper.js const clickSelectors = (scenario.clickSelectors || [scenario.clickSelector]).filter(x => x); clickSelectors.forEach((clickSelector) => { console.log(`INFO: click ${clickSelector}`); chromy .wait(clickSelector) .click(clickSelector); });
(Chromyを利用しているが、現時点でBackstopJSはPuppeteerにも対応しているので、そっちでも利用できるようにしたい…)
UserAgent を設定する
viewport
の設定による画面サイズごとのテストはサポートしているが、標準ではそこにUAを付けてくれない。
なので、 viewport
の内容を見てUAを切り替える処理を自分で書き加える。
backstop.json
の onBeforeScript
に指定されているスクリプトで、以下を記述。
// backstop_data/engine_scripts/chromy/onBefore.js if (vp.label === "phone") { chromy.userAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"); } else { chromy.userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"); }
Cookieの読み込み動作周り
初期状態では、Cookieは毎回 cookiePath
オプションでテストシナリオごとに指定しなければいけないが、ログインが前提のアプリケーションでは使いづらい。
デフォルトのCookieを読み込むように変更した。
// backstop_data/engine_scripts/chromy/loadCookies.js const defaultCookiePath = "backstop_data/engine_scripts/cookies.json"; const cookiePath = scenario.cookiePath === "" ? "" : scenario.cookiePath || defaultCookiePath;
また、BackstopJSが生成したクッキー読み込みスクリプト loadCookies.js
ではテストシナリオごとにCookieの中身を出力するので、ログが流れてしまい追いづらい。
必要ないときは出力を抑制するようにした。
// backstop_data/engine_scripts/chromy/loadCookies.js if (process.env.VERBOSE) { console.log("Cookie state restored with:", JSON.stringify(cookies, null, 2)); } else { console.log("Cookie state restored."); }
テストシナリオの複数JSONへの分割機能
BackstopJSは標準ではテストシナリオを backstop.json
に以下のように他の設定と同列に書く。
scenarios: [ { "label": "xxx", "url": "http://localhost:3000/xxx", "selectors": [ ".aListOfStuff li" ] } ]
これだと、テストシナリオが増えたときに管理しづらくなる。
そのため backstop_data/scenarios
内の複数のJSONファイルに分割して管理できるようにした。
実行時にはこれらのJSONをそれぞれ読み込み、以下のようにひとまとめにする( backstop_data/scenarios/index.json )。
const fs = require("fs"); const jsonFiles = fs.readdirSync(__dirname).filter(filename => filename.match(/\.json$/)); const scenarios = jsonFiles.reduce((acc, filename) => ( acc.concat(require(`./${filename}`)) ), []); module.exports = scenarios;
こうしてひとまとめになったJSONをBackstopJSに渡す、という処理を backstop_data/runner.js
に記述し、yarnスクリプトから呼び出すようにした。
(runner.js
については後述)
backstop_data/runner.js
const backstop = require("backstopjs"); const config = require("../backstop.json"); const scenarios = require("./scenarios"); config.scenarios = scenarios; backstop(command, { filter, config });
package.json
{ "scripts": { "backstop": "node ./backstop_data/runner.js" } }
テストシナリオのJSONへの変数展開機能
例えば商品詳細画面のように、テスト対象のURLには動的なものもある。 そのようなURLを決め打ちでJSONに記述すると、以下のようにテスト環境が変化したときにURLが無効になってしまう。
- URLのパラメータにしていたレコードの ID, UUID, 名前, etc が変わる
- レコードが削除される
このような環境変化をある程度吸収するために、JSON読み込み時に変数展開を行えるようにした。
たとえば次のように、 ${}
で埋め込んだ変数を展開してテスト対象URLを動的にできる。
{ "url": "http://localhost:3000/posts/${post_id}" } post_id が 1 なら… => { "url": "http://localhost:3000/posts/1" }
具体的な実装はシンプルにこんな感じ( backstop_data/interpolate.js )。
const variables = require("./variables"); const keys = Object.keys(variables); module.exports = (json) => { const jsonStr = JSON.stringify(json); const interpolated = keys.reduce((acc, key) => ( acc.split("${" + key +"}").join(variables[key]) ), jsonStr); return JSON.parse(interpolated); };
これを runner.js
が呼び出して、BackstopJSに渡す前に適用している。
変数を展開するための値は、 backstop_data/variables.json
に定義するようにした。
{ "post_id": 1, ... }
また、このJSONを手動で変更するのはつらいので自動で生成するスクリプトを書いた。 今回はRailsプロジェクトなので、次のようなRakeタスク。
desc 'Dump a JSON file for the backstop variable interpolation' task dump_backstop_variables: :environment do post = Post.first! json = JSON.pretty_generate({ post_id: post.id.to_s }) File.open('backstop_data/variables.json', 'w') { |f| f.write(json) } end
透過的なyarnスクリプトを用意する
次のように、yarnスクリプトを利用する場合でも、BackstopJSのサブコマンドやオプションを透過的に利用できるようにしたかった。
$ yarn run backstop test $ yarn run backstop test --filter="<scenario label>" $ yarn run backstop reference $ yarn run backstop approve
JSONを結合したり、JSONに変数を展開したりといった機能を実現するために runner.js
を作成したので、yarnスクリプトから引数を受け取って、うまいことBackstopJSの関数に渡す必要がある。
パーサをrequireするのも良いのだが、シンプルな用途なのに依存を増やすのもあれだったので、自分で軽く書いた。
ハマリポイント
スクリーンショットの撮影に失敗しやすい
テストレポートにスクリーンショットが出ない場合、撮影に失敗している。
サーバーが起動していて、かつ selector などの指定が明らかに正しい場合は、
backstop.json
の asyncCaptureLimit
の値を下げてみると改善する。
この値はヘッドレスブラウザの並列起動数で、メモリが足りなかったりすると撮影に失敗することがあるようだ(詳しくは調べていない)。
他にも、サーバー立ち上げ直後だとアセットの更新(webpackだったり、Railsならアセットパイプラインだったり)で時間がかかりタイムアウトする場合がある。
事前にページを開いておく、webpack
、bin/rails assets:precompile
しておくなどでテストを安定化できる。
デザイン崩れに起因しない差分が出やすい
テストの性質上、冪等性が保ちづらい。 デザインは崩れていなくても、スクリーンショットに差分が出ることが多くある。 例えば、
- レコードの増減、変更
- 時間経過による動的な表示
- ランダム表示
などの影響を受けやすい。
あまりに動的で毎回差分が出るような要素はあらかじめ hideSelectors
で非表示にすると良い。
もしくは特定要素のみの確認で良ければ selector
でその他の要素の影響を避けることもできる。
差分が出て、デザイン崩れに起因しなければ、その都度 backstop approve
する。
Ruby25の聴講メモ
昨日(2/24)、Ruby25に参加してきました。 最初は小さくお祝いするのかなと思ってたら、予想以上に大きなカンファレンスでびっくりしました。参加できてよかった。 いろいろあって最近仕事でRubyをあまりキメられていないのですが、やっぱ良い言語で良いコミュニティだなと再確認しました。 また、久々に会えた方々とお話できて良かったです。
以下、聞いた講演のメモです(まとめられてないです)。 聞き間違えや解釈の違い等あるかもしれません。
高橋さん「Rubyの1/4世紀」
- 「Ruby」の頭文字が大文字なのは、連載「ちょーわかりやすい Perl & Ruby 入門」で「ruby」のように小文字だと「Perl」との並びが悪いので大文字になったらしい
- かつて主要なパッケージ管理にはRPA, RubyGemsがあり互換性なかった。RailsがRubyGmesを採用したことで普及
- rubygems.org は Gemcutter が前身
- パッケージ管理やbundlerなど、Rubyのエコシステムの構築をRailsがリード
Matz「Ruby after 25 years」
この25年をふりかえる
- プラットフォームとしてのOSはほぼOS。WindowsすらWSL
- CPUもあんまりアーキテクチャが変わらない
- プラットフォームは多様性が減少する一方
- コンピュータの普及、性能の向上、モバイルの台頭
- サーバサイドでなんでもやる時代ではなくなった。モバイルアプリ、SPA
- トレンドの変化がある
- 変化の傾向として、ものすごくスケーラブルに、分散処理
これを踏まえて未来のRubyは?
- Rubyのコアは変わらないのでは
- プログラミング言語のテーマは生産性の向上
- より早くより簡潔に書ける、頭の中を直接コンピュータに伝えられる
- 優れた抽象モデルが提供できる
- 保守性がある
- 現在のRubyは小さなチームではじめられる
近未来のRuby(Ruby3)
- 高速、分散、解析
- Ruby 3x3
- やればできるゴールではなく、できるかもしれないゴール、わくわくさせるようなことを提示するのが私の役割
- 言ってみるもんで、案外できそうなところまできた
- 2020年目標(約束はしない)
- 過去に学んで、断続的な変化はしない。Ruby3は単なるラベルにする
その先のRuby
- インタラクティブプログラミング
- 入力したら警告がポップアップで出てくるみたいな感じ
- 静的解析技術、実行時プロファイリングなどの発展系
- 賢いテディベア(テディベアプログラミング)がコンパイラについてくるなど
- 分散技術の発展とともに、分散処理の抽象化、スケーラブルなアーキテクチャが必要になってきている
- Webもいい線行っているが万能ではない
- FaaSが現在では近いと予想
- Rubyでは、Guildのその先ができるといい
- 非均質的計算環境対応
むすび
- 人類は25年ではあまり変わらないが、文化は変化する
- 最大の目標は「プログラミング言語サバイバル」
- 生き残るために価値、楽しいプログラミングを提供し続ける
松田さん「Ruby on Ruby on Rails 」
- RailsはRubyの良さを伝えるためのインフラ
- 先週くらいから6.0を開発中
- 5.2はコミッタの中では終わったRails
- これからもRailsは変わり続け世の中を変え続けるので必死こいてバージョンアップして下さい
近藤さん「 RubyとInfrastructure as Code、そして大規模インフラ」
- Rubyはどう Infrastructure as Code と関わっていくか
- RubyはDSLを作るのに向いている
- なのでDSLを採用したプロダクトが多い(Cheff, Itamae, Serverspec...)
- 特にConfigurationのツールが多い
- 直近のインフラコードの潮流
- マイクロサービス、コンテナ化、サーバレス
- ウェブインフラの大規模化、アプリの複雑さが背景
- 支援団体 Cloud Native Computing Foundation(代表プロジェクト: Kubernetes)
石井さん「 mruby、今IoT、組込界隈でこう使われています、最新事例紹介!」
- 組み込み技術者不足
- webの人にも組込してもらえたら良いよね、ということでmruby
- ruby, mruby, mruby/c で世の中のなんでも作れる
- 試作しやすさ、文字列処理のしやすさ
- ただし弱点として何msec以内に〜をしなければ、というのを保証できない
- それ以外のエンタメとかいろいろな面で利用価値がある
- しかし、なかなか使ってもらえなかった
- 組み込みは一回製品を出荷したら変更できないのですごく慎重
- 軽量Ruby 普及・実用化促進ネットワークで技術情報を提供中
- これからのmruby
- Ruby2.0をキャッチアップ(現在1.9ベース)
- グローバル会議やりたい(来年)
田籠さん「Data Processing and Ruby in the World」
データ処理のステップは collect => summarize => analyze => visualize
この世界でのRuby
という感じになっている
- Visualizeの世界はプロプライエタリで、Rubyの有力なソフトウェアがない
- fluentdはkubernetesと相性がいい(kubernetesはGCP, Azure, AWSそれぞれホスティングされてる)
これからの機械学習系プロジェクト
他の言語ではすでに実装されているものもあるが、全体のエコシステムで不自由なく使えることがとても大事。
Matz & Miyagawa 未来を語る特別対談
RailsのRoutingの名前付けでハマった
Astrum などのスタイルガイドジェネレータを利用する際、Railsの外からコンパイル済みCSSを読み込みたいことがある。
このとき、アセットパイプラインを利用している場合はdigestを含めて参照する必要があるのだが、Railsの外の世界で、かつ変更に開いていない場合いい感じにdigestを埋め込むことができない。
そこで今回はRailsのコントローラ経由でCSSをリダイレクトしてしまうアイデアで実現したのだが、Routingのリソース名でハマった。
class AssetsController < ApplicationController def show redirect_to helpers.stylesheet_url('hogehoge') end end
resource :asset, only: :show
こうするとなにがまずいかというと、 asset_url
が上書きされてしまうのだ。
従って一部のアセットが正しく取得できなくなる。 SassのURLヘルパーもこのメソッドを利用しているので 1、CSSに埋め込んだimageやfontが軒並み表示されてなくて謎だった。
かなりレアケースではあると思うが2、Railsでは内部で使われていそうな名前のRoutingは避けようという教訓を得た…。
そして、こういうときにfinalistのような意図しないoverrideを防ぐ手段があればなあと思うのであった。ただ、Ruby, Rails的には受容されない気もするが。
Routingに限らず、動的生成されるメソッドには油断しないようにしたい。
Jbuilder の generator を無効にする
故あってサンプルコードをたくさん作る必要があり、JBuilderの生成するコードがじゃまになる(けど使うので消せない)ときがあった。 そんなときに行った対策を簡単にまとめてみる。
.json.jbuilder ファイルの生成を無効化
config/application.rb
に以下を記述。
config.generators do |g| g.jbuilder = false end
経験上、他に不要なものも併せて以下のように記述することが多い。
config.generators do |g| g.system_tests = nil g.jbuilder = false g.assets = false g.helper = false end
コントローラのテンプレート上書き
コントローラでは例えば以下のように、content type による振り分けが生成される。
# POST /users # POST /users.json def create @user = User.new(user_params) respond_to do |format| if @user.save format.html { redirect_to @user, notice: 'User was successfully created.' } format.json { render :show, status: :created, location: @user } else format.html { render :new } format.json { render json: @user.errors, status: :unprocessable_entity } end end end
これは以下でRails標準のテンプレートが上書きされるため。
https://github.com/rails/jbuilder/blob/master/lib/generators/rails/templates/controller.rb
無効にするには、以下の手段がある。
- Rails標準のテンプレートを、
lib/rails/generators/rails/scaffold_controller/templates/controller.rb
にコピーする - 生成するときだけ一時的に Gemfile から jbuilder をコメントアウトする
Rails 5.0.2 => 5.1.4 へのアップデートで地味に面倒くさかったこと
ActionController::Parameters がpermittedでないときに例外出すようになった
ちゃんと指定してpermitしたほうが良いんだろうけど、ログとかでpermitしなくても問題ない場面やpermit対象が動的に変わるものがある。 そういときに
params[:hoge].to_h
みたいなのはpermittedフラグが立ってないので例外が発生する。
params[:hoge].permit!.to_h # もしくは params[:hoge].to_unsafe_h
のようにする。
ActiveRecord::AttributeMethods::Dirty の一部仕様が変わった
save前後で呼び出す attribute methods が変わった。 https://github.com/rails/rails/pull/25337
使い分けは面倒だが、save前後での違いが明確になってスッキリした感じ。
リリースノートにないので注意。
save後
attribute_changed?
とかをsave後に利用することがdeprecatedとなった。
after_save コールバック内で呼び出したりするのも同じ。
たとえば以下のような感じに対応する。
attribute_changed?
=>saved_change_to_attribute?
attribute_was?
=>attribute_before_last_save
※ attribute method なので attribute
の部分をカラム名で置き換える。nameの場合は saved_change_to_name
のようになる。
save前
今までと同じ感じで使える。
ただ、save前用の新しいメソッドも生えてる。
たとえば attribute_changed?
に対しては will_save_change_to_attribute?
が使える。
新しい方は内部的な扱い方がちょっと違うけどやってることは同じっぽい。
create_table のプライマリキーがデフォルトBIGINTになった
ActiveRecord::Migration[5.1]
以降を継承しているマイグレーションが対象。
ActiveRecord::Migration[5.0]
以前のバージョンのマイグレーションはBIGINTにならないよう互換性の配慮がされている。
具体的には schema.rb
での create_table
のオプションに id: :serial
(pgの場合)が付くようになる。
5.0以前と以後のマイグレーションではプライマリキーの型で違いが出るようになってしまう。 これに対して2通りのアプローチがある。
- Migration[5.0] 以前で作ったプライマリキー(及びそれを参照する外部キー)も順次BIGINTにしていく
- Migration[5.1] 以降のマイグレーションではすべて
id: :integer
をつけるルールにする(デフォルトに従わない)
1 が順当そうで、このPR投げた人もおすすめしてる ( http://www.mccartie.com/2016/12/05/rails-5.1.html )。 BIGINTが必要にならない場合は 2 もありなんだけど、ルールを忘れられないようにするか、自動でなんかするかしたほうがいい。
BIGINTとINTが混ざった状態も許容するのも考えられるが、後々の負の遺産となりそうなのでどちらかに統一したほうがよさそう。
互換性の考慮漏れが一部ある(Rails 5.1.4)
ふつうに create_table
していれば前述したような互換性対策が走るので問題ないのだけど、一部考慮が漏れているところがある。
具体的にはプライマリキーを add_column :articles, :id, :primary_key
のようにして作ったとき、BIGINTになってしまう。
masterでは対応が入っている ようなので、Rails 5.1.5 では問題にならなさそうだけど、
一応それまでは db:migrate:reset
によるschema全更新ができないことは注意したい。
Webpacker 3.0 でのWebpackの設定カスタマイズ
Webpacker 3.0 以降ではWebpackの設定を直接書かずに、npmモジュールに定義されるデフォルトを上書きする形になった。 いい感じにデフォルトがそのまま使えて楽になった反面、すでに設定を結構いじったりしている場合にバージョンアップをするのがちょっと面倒だった。
カスタマイズの基本的な方法はドキュメントに書いてある。
https://github.com/rails/webpacker/blob/v3.0.1/docs/webpack.md
が、書いてあること以外で考えたことを以下にメモ。
環境ごとにPlugin設定の一部を上書き
// config/webpack/production.js const environment = require('./environment'); const UglifyJsPlugin = environment.plugins.get('UglifyJs'); UglifyJsPlugin.options.sourceMap = false; module.exports = environment.toWebpackConfig();
プラグインのoptionsオブジェクトに入っているのを上書きする。 Webpackプラグインの作りは理解してないけど、おそらくこのoptionsがあるという構造は統一されているのかな。
なお、この例ではプロダクションでソースマップを無効にしている。 (ソースマップ生成って結構パワー使うので)
一度プラグインを消しちゃって追加し直す方法もある。ぜんぜん違う設定にするときは楽かも。
参考: https://github.com/rails/webpacker/issues/771
environment.plugins.delete('UglifyJs') environment.plugins.set( 'UglifyJs', new UglifyJsPlugin({ sourceMap: false }) )
動作確認は NODE_ENV
環境変数を利用するとかんたん。
$ NODE_ENV=production bin/webpack
staging環境
npmパッケージからデフォルトで提供されるenvironmentsは、 development, test, production のみ。 それ以外の環境はまっさらなEnvironmentオブジェクトがそのまま渡ってくる。
https://github.com/rails/webpacker/blob/v3.0.1/package/index.js#L10
めんどうなのでproductionと同じにしたいのだけれども、デフォルトで参照するenvironmentは環境変数 NODE_ENV
で切り分けるため、NODE_ENV=production
で起動する以外なさそう。
とはいえstaging固有の設定もしたいわけで、ひとまず以下のようにproduction相当の設定をべた書きする形となった。
const environment = require('./environment'); const webpack = require('webpack'); const CompressionPlugin = require('compression-webpack-plugin'); environment.plugins.set('ModuleConcatenation', new webpack.optimize.ModuleConcatenationPlugin()); environment.plugins.set('UglifyJs', new webpack.optimize.UglifyJsPlugin({ sourceMap: true, compress: { warnings: false, }, output: { comments: false, }, })); environment.plugins.set('Compression', new CompressionPlugin({ asset: '[path].gz[query]', algorithm: 'gzip', test: /\.(js|css|html|json|ico|svg|eot|otf|ttf)$/, })); module.exports = environment.toWebpackConfig();
もうちょっとうまい感じの方法はないかな…。
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.hoge
と foo
キーを指定して書かねばならない。
なぜなら 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側で吸収したほうが良さそう。