自殺者統計チャート描画サイトをつくった際の気付き(React, React-Router, ちょっとRails)
こんなかんじです
:link: 自殺を知る、自殺を考える::職業別の自殺者数を年度で並べて表示 :link: 自殺を知る、自殺を考える(トップページ)
Reactまわり
うすくやっていく
前回はRedux+Redux-Routerを使ってうまくコンテキストを分離できてよかったですね、ということになりました。
:link: やりなおしRedux - Redux Routerでコンテキストをわけると楽になる - Qiita
しかし、さあまたなにか作りましょうとなったときに、あのActionsやReducersやconnectを思いうかべただけでもういやになるという、どうしようもないめんどくささがあります。
というわけで、今回は**React の Context を使って Flux を実装する - Qiita**をほぼそのまま使うことにより作業を軽くすることができました。 :metal:
Provider
が各コンテキストの親分となり、自分が監視するemitter
を下々のDispatchableComponent
に与えます。
下々はemitter
にdispatch
することが主な仕事であり、ほとんど唯一の仕事となります。
ビューとして身軽
これのいいところは、DispatchableComponent
はProvider
になにを言っていいかを知っている必要がなく、Provider
もまたDispatchableComponent
がなにを言いだすか必ずしも知っている必要がないというところです(個人の感想です)。
メソッドをバケツで与え与えられる必要もなく、好き勝手にdispatch
するだけです。
DispatchableComponent
は誰が処理するかは知らないけどとにかくdispatch
して、コンテキスト内の処理であれば直属のProvider
が、アプリケーション全体での横断的なアレであれば最上位のProvider
が処理するという寸法です。
入れ子
今回はこれのProvider
をDispatchableComponent
サブクラスにして、上からemitter
をバケツされた場合はそれを使い、アプリケーション全体でひとつのemitter
を使う実装にしました。
(なので、各Provider
がemitter
を持っていた時とは違い、componentWillUnmount
時にremoveListener
しなければいけなくなりました。最初これに気づかず、1dispatch
に対して何度も処理が走ってしまいました)
Provider
もまた上にProvider
がいる場合はDispatchableComponent
として振る舞います。
コンテキスト外へ何かを投げる場合は直属のProvider
の頭越しにダイレクトには行わず、一度直属のProvider
を経由したほうが混乱は少ないかもしれません。
コンテキストとビューとReact-Router
コンテキスト(Provider
)が自分のstate
や他から引っぱってきたデータ、そして自分より上から降ってきたprops
を加工し、ビュー(DispatchableComponent
)に対してprops
として配給します。ビューは基本的にstate
を持ちません。
TypeScriptでいうとビューは常にこんな感じになります。
class View extends DispatchableComponent<P,{}>{
...
}
ビューはなにをするにもdispatch
し、それだけです。なにかが変わるか変わらないかはまったくしりません。
dispatch
してprops
が変わればそれを描画します。
コンテキストをはさむためにReact-Router
React-Routerはパスが一致したコンポーネントを、入れ子の上にあるコンポーネントにprops.children
としてわたす機能があります。
<Route path="chart" component={ChartContext}>
<Route path="" component={ChartController}>
...
</Route>
</Route>
// ChartControllerが描画される
render(){
return <section>
{this.props.children}
</section>
}
この機能を利用して、Provider
はDispatchableComponent
へprops
を渡すことに専念させます。
render() {
let props = _.merge(_.clone(this.props), this.state);
delete props.children;
return React.cloneElement(this.props.children || <div>blank</div>, props);
}
このようにReact-Routerを利用することにより、誰の中になにがはいるかの記述を、コンテキストやビューから分離できます。
props.children
になにが入りうるかはReact-Routerのみが知ればよく、ビューのソース内にコンテキストがあらわれたり、コンテキストのソース内にビューがあらわれたりしません。
これができたらいいのにな
パスが一致する全てのコンポーネントがprops.children
に並列で入ってくれると便利だったのですが、それはできないようです。
おかげでRoute
記述にちょっとした継承地獄っぽい趣が出てきてしまって、これ最高!とはなりませんでした。要研究。
チャートの描画
以下のライブラリを使いました。
:link: A Javascript Library For Building Composable And Declarative Charts | React-D3
本当はチャートの描画(とその覚え書きを書く)目的で作りはじめたサイトだったのですが、整形したデータを渡すだけで描画されるので、特に書くことがありませんでした。
具体的なサンプルコードと、そのサンプルデータがどこにあるか最初わからなかったので、それだけメモっておきます。
データの整形
今回はメインのデータとして年齢層、手段、原因といった自殺者全体を詳細項目で分類したデータが、年度、性別、地域ごとに用意されている状態でした。(6年度3性別49地域と10詳細項目で9000rows近くなり、Herokuでギリギリだったのはまた別の話)
チャート作成の素養がまったくなかったので、詳細項目がY
になるのかX
になるのか、はたまた表ごとの分類時になるのか(なんと言い表せばいいのかさえわからない)さっぱりわからなく、最初は詳細項目が入る時点でif
分岐させており、おかげでどの段階で分類されるかによりコードがことなり、それだけでコード行数が増えてしまいました。
詳細項目ごとにテーブルを作成し、年度、性別、地域はbelongs_to
として持たせているので、たとえばReason
クラスからはこういう形のレコードを得ることができます。
{
year: {name: '平成26年'},
gender: {name: '男'},
area: {name: '大阪'},
family: 112,
health: 541,
life: 233,
work: 108,
partner: 41,
school: 16,
other: 26,
unknown: 64,
}
これをこのまま/year/area/reason/gender
の順番でグループ化したり、/reason/area/year/none
でグループ化したりしようとすると、reason
が入る位置によって処理が分岐してしまいました。
が、ああ、同じ形にすればいいんだと気づいて、このように変形してからグループ化することにより、分岐を取り除き、単なる再起っぽい処理でグループ化することができました。
{
year: {name: '平成26年'},
gender: {name: '男'},
area: {name: '大阪'},
reason: {name: 'family'},
value: 112
}
{
year: {name: '平成26年'},
gender: {name: '男'},
area: {name: '大阪'},
reason: {name: 'health'},
value: 541
}
{
year: {name: '平成26年'},
gender: {name: '男'},
area: {name: '大阪'},
reason: {name: 'life'},
value: 233
}
...
メモリ的にどないやねんとか処理速度的にどないやねんとかありましたが、極めて単純で失敗がない処理内容に落としこめたので、これでよしとしました。
Railsでのテスト
今回は内閣府の統計データを登録してそれを表示するということで、データの変更はありません。
決まりきったデータを表示するテストが主となるので、毎回登録するのは無駄であるから、統計データは登録しておいてそれをそのままテストで使いたい。
一方で、登録処理のテストをもちろんしておきたいというのがありました。
テストにより使用するデータベースを切りかえる
登録データを使用するテストは通常のテストテーブルを使うとして、登録テスト時にはSQLiteのオンメモリデータベースを使うことにしました。
該当テスト前にSQLiteに切りかえ、終わればもとに戻すだけですが、establish_connection
ごとにデータベースがまっさらになるので切りかえ毎にload "#{Rails.root}/db/schema.rb"
するのがコツといえばコツです。
test:
<<: *default
database: total_suicides_test
test_sqlite:
adapter: sqlite3
database: ":memory:"
pool: 5
timeout: 5000
describe Importer do
before :all do
ActiveRecord::Base.establish_connection(:test_sqlite)
load "#{Rails.root}/db/schema.rb"
end
after :all do
ActiveRecord::Base.establish_connection(:test)
end
...
end
おわり
今回はデータをどういう流れで形にすればいいのか最初さっぱりわからなくて、こうかな?こうかな?と何度も組みなおしていたせいで、できあがったものはごく小さいものながらも、かなり時間がかかってしまいました。
初期ではRailsは単にレコードをそのまま返すだけしかしておらず、データの整形やその他もろもろはすべてJavaScript側で行っていました。
しかしデータの整形はデータに近い位置でやるのがいいのではと思いなおしてRails側に移動したぐらいから、だんだんと、同じ処理で回すには同じ形に下ごしらえしておけばいいのでは、などの気付きが発生し、なんとなく整理ができた気がします。