自殺者統計チャート描画サイトをつくった際の気付き(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側に移動したぐらいから、だんだんと、同じ処理で回すには同じ形に下ごしらえしておけばいいのでは、などの気付きが発生し、なんとなく整理ができた気がします。