reactにfluxを採用してアプリケーションを設計していると、親->子->孫コンポーネントへpropsのバケツリレーになってしまう事がありました。
そもそもイケてない設計が悪いのですが、reactバケツリレーつらいという声を聞くことも多かったし、自分も辛いと思っていたところ助言をいただいて解決法が見えてきたのでまとめてみます。
propsバケツリレー
まず、バケツリレーを再現してみます。
DOM構造
おおまかにdiv[class="app"]
(親), div[class="group"]
(子), div[class="item"]
(孫)というツリー構造になってます。
<div class="app"><!--親-->
<div class="group"><!--子-->
<h2>1番目のグループ</h2>
<p class="count">13</p>
<div class="items">
<div class="item"><!--孫-->
...
</div>
<div class="item"><!--孫-->
...
</div>
...
</div>
</div>
<div class="group"><!--子-->
<h2>2番目のグループ</h2>
<p class="count">5</p>
<div class="items">
<div class="item"><!--孫-->
...
</div>
<div class="item"><!--孫-->
...
</div>
...
</div>
</div>
...
</div>
で、これをComponent化するとそのまま<App />
, <Group />
, <Item />
みたいに分けることができるんじゃないかと考えます。
Storeが保持するデータ構造
Storeが保持するデータ構造も同じで複数のgroup(子)にぶら下がるitem(孫)達という感じです。
{
"group-1": [{item1-1}, {item1-2}, {item1-3}, ...],
"group-2": [{item2-1}, {item2-2}, {item2-3}, ...],
...
}
App(RootComponent)
実際のComponentはどうなるかというと、
AppはStoreをsubscribeするRootComponentとして実装されこんな感じ。
var Group = require("./Group.jsx");
var App = React.createClass({
getInitialState(){
return {
groups: Store.getAll()
}
},
render(){
var groups = Object.keys(this.state.groups).map((key) => {
return <Group items={this.state.group[key]} title={key} />;
});
return (
<div className="app">
{groups}
</div>
);
},
...
});
Group
GroupからItemへ値のリレー
var Item = require("./Item.jsx");
var Group = React.createClass({
render(){
var items = this.props.items.map((item) => {
return <Item {...item} />
});
return (
<div className="child">
<h2>{this.props.title}</h2>
<p className="count">{this.props.items.length}</p>
<div className="items">
{items}
</div>
</div>
);
}
});
このようにDOM構造と同じだけReactComponentも入れ子していってしまうと、
propsのバケツリレーで面倒なだけでなく、Itemへ渡したい値は必ずGroupを通らねばならず、GroupはItemの設計に依存してしまいます。
これではGroupはテストも作りにくく変更にも弱いですね。
propsのバケツリレーを無くすために
依存を無くす
子孫間の依存を無くすために、<Item />
に渡すpropsを<App />
から直接渡す形に変更します。
var Group = require("./Group.jsx");
var Item = require("./Item.jsx");
var App = React.createClass({
getInitialState(){
return {
groups: Store.getAll()
}
},
render(){
return (
<div className="app">
{this.renderGroups()}
</div>
);
},
renderGroups(){
var groups = Object.keys(this.state.groups);
return groups.map((key) => {
return (
<Group count={groups[key].items.length} title={key}>
{this.renderItems(key)}
</Group>
);
});
},
renderItems(key){
var items = this.state.groups[key];
return items.map((item) => {
return <Item {...item} />;
});
},
...
});
var Group = React.createClass({
render(){
return (
<div className="group">
<h2>{this.props.title}</h2>
<p className="count">{this.props.count}</p>
<div className="items">
{this.props.children}
</div>
</div>
);
}
});
変更のポイントは<Group>{items}</Group>
という形で<Item />
(の集合)を直接Componentとして渡すことです。
このように配置された場合、itemsは<Group />
のthis.props.children
に入るので、そのまま任意の場所に流し込めばよいでしょう。
この変更により<Group />
に渡されるComponentは何の縛りもなくなり、ただ自分の管轄であるtitleとcountだけを受け取ればいい形になりました。
当たり前にやってる人にはなんの驚きもないとおもいますが、自分はそもそもpropsにComponentを渡すという発想が希薄だったので、これは目から鱗でした。アドバイスを下さった @axross_ さんに感謝m(_ _)m
もっとこうしたほうがいいよとかありましたら、是非コメントをください。