いくつかの経験則に基づいて、facebook Flux と Immutable.js を使用した私の実装は、あなたの懸念の多くに対応していると思います。
店舗
APIユーティリティ
- 私は ajax 呼び出しにSuperAgentを使用しています。各リクエストは
Promise
- URL + params のハッシュによってインデックス付けされた読み取りリクエストのマップを使用します
Promise
Promise
解決または拒否されたときに、fooReceived や fooError などの ActionCreators を介してアクションをトリガーします。
fooError
アクションには、サーバーから返された検証エラーを含むペイロードが含まれている必要があります。
コンポーネント
- コントローラ ビュー コンポーネントは、ストアの変更をリッスンします。
- コントローラー ビュー コンポーネント以外のすべてのコンポーネントは「純粋」であるため、ImmutableRenderMixinを使用して、本当に必要なものだけを再レンダリングします (つまり、
Perf.printWasted
時間を出力する場合は、非常に低く、数ミリ秒になるはずです)。
- RelayとGraphQLはまだオープン ソース化されていないため、.
props
propsType
- 親コンポーネントは、必要な小道具のみを渡す必要があります。親コンポーネントが (この例を簡単にするためにここで
var fooRecord = { foo:1, bar: 2, baz: 3};
は使用していません)などのオブジェクトを保持し、子コンポーネントがandを表示する必要がある場合、オブジェクト全体を渡すのではなく、子コンポーネントに小道具として渡すだけです。他のコンポーネントが値を編集する可能性があるため、このコンポーネントはこの値をまったく必要としませんが、子コンポーネントを再レンダリングします!Immutable.Record
fooRecord.foo
fooRecord.bar
foo
fooRecordFoo
fooRecordBar
foo.baz
ROUTING
- 私は単にReactRouterを使用しています
実装
基本的な例を次に示します。
API
apiUtils/Request.js
var request = require('superagent');
//based on http://stackoverflow.com/a/7616484/1836434
var hashUrl = function(url, params) {
var string = url + JSON.stringify(params);
var hash = 0, i, chr, len;
if (string.length == 0) return hash;
for (i = 0, len = string.length; i < len; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
var _promises = {};
module.exports = {
get: function(url, params) {
var params = params || {};
var hash = hashUrl(url, params);
var promise = _promises[hash];
if (promise == undefined) {
promise = new Promise(function(resolve, reject) {
request.get(url).query(params).end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
_promises[hash] = promise;
}
return promise;
},
post: function(url, data) {
return new Promise(function(resolve, reject) {
var req = request
.post(url)
.send(data)
.end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
};
apiUtils/FooAPI.js
var Request = require('./Request');
var FooActionCreators = require('../actions/FooActionCreators');
var _endpoint = 'http://localhost:8888/api/foos/';
module.exports = {
getAll: function() {
FooActionCreators.receiveAllPending();
Request.get(_endpoint).then( function(res) {
FooActionCreators.receiveAllSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveAllError(err);
});
},
get: function(id) {
FooActionCreators.receivePending();
Request.get(_endpoint + id+'/').then( function(res) {
FooActionCreators.receiveSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveError(err);
});
},
post: function(fooData) {
FooActionCreators.savePending();
Request.post(_endpoint, fooData).then (function(res) {
if (res.badRequest) { //i.e response return code 400 due to validation errors for example
FooActionCreators.saveInvalidated(res.body);
}
FooActionCreators.saved(res.body);
}).catch( function(err) { //server errors
FooActionCreators.savedError(err);
});
}
//others foos relative endpoints helper methods...
};
店舗
ストア/BarStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../APIUtils/BarAPI')
var CHANGE_EVENT = 'change';
var _bars = Immutable.OrderedMap();
class Bar extends Immutable.Record({
'id': undefined,
'name': undefined,
'description': undefined,
}) {
isReady() {
return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
}
getBar() {
return BarStore.get(this.bar_id);
}
}
function _rehydrate(barId, field, value) {
//Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
_bars = _bars.updateIn([barId, field], function() {
return value;
});
}
var BarStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_bars.has(id)) {
BarAPI.get(id);
return new Bar(); //we return an empty Bar record for consistency
}
return _bars.get(id)
},
getAll: function() {
return _bars.toList() //we want to get rid of keys and just keep the values
},
Bar: Bar,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
var _setBar = function(barData) {
_bars = _bars.set(barData.id, new Bar(barData));
};
var _setBars = function(barList) {
barList.forEach(function (barData) {
_setbar(barData);
});
};
BarStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.BAR_LIST_RECEIVED_SUCESS:
_setBars(action.barList);
BarStore.emitChange();
break;
case ActionTypes.BAR_RECEIVED_SUCCESS:
_setBar(action.bar);
BarStore.emitChange();
break;
case ActionTypes.BAR_REHYDRATED:
_rehydrate(
action.barId,
action.field,
action.value
);
BarStore.emitChange();
break;
}
});
module.exports = BarStore;
ストア/FooStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/FooConstants').ActionTypes;
var BarStore = require('./BarStore');
var FooAPI = require('../APIUtils/FooAPI')
var CHANGE_EVENT = 'change';
var _foos = Immutable.OrderedMap();
class Foo extends Immutable.Record({
'id': undefined,
'bar_id': undefined, //relation to Bar record
'baz': undefined,
}) {
isReady() {
return this.id != undefined;
}
getBar() {
// The whole point to store an id reference to Bar
// is to delegate the Bar retrieval to the BarStore,
// if the BarStore does not have this Bar object in
// its cache, the BarStore will trigger a GET request
return BarStore.get(this.bar_id);
}
}
function _rehydrate(fooId, field, value) {
_foos = _foos.updateIn([voucherId, field], function() {
return value;
});
}
var _setFoo = function(fooData) {
_foos = _foos.set(fooData.id, new Foo(fooData));
};
var _setFoos = function(fooList) {
fooList.forEach(function (foo) {
_setFoo(foo);
});
};
var FooStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_foos.has(id)) {
FooAPI.get(id);
return new Foo();
}
return _foos.get(id)
},
getAll: function() {
if (_foos.size == 0) {
FooAPI.getAll();
}
return _foos.toList()
},
Foo: Foo,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
FooStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.FOO_LIST_RECEIVED_SUCCESS:
_setFoos(action.fooList);
FooStore.emitChange();
break;
case ActionTypes.FOO_RECEIVED_SUCCESS:
_setFoo(action.foo);
FooStore.emitChange();
break;
case ActionTypes.FOO_REHYDRATED:
_rehydrate(
action.fooId,
action.field,
action.value
);
FooStore.emitChange();
break;
}
});
module.exports = FooStore;
コンポーネント
components/BarList.react.js (コントローラー ビュー コンポーネント)
var React = require('react/addons');
var Immutable = require('immutable');
var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
barList: BarStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
BarStore.removeChangeListener(this._onChange);
},
render: function() {
var barItems = this.state.barList.toJS().map(function (bar) {
// We could pass the entire Bar object here
// but I tend to keep the component not tightly coupled
// with store data, the BarItem can be seen as a standalone
// component that only need specific data
return <BarItem
key={bar.get('id')}
id={bar.get('id')}
name={bar.get('name')}
description={bar.get('description')}/>
});
if (barItems.length == 0) {
return (
<p>Loading...</p>
)
}
return (
<div>
{barItems}
</div>
)
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
コンポーネント/BarListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
// I use propTypes to explicitly telling
// what data this component need. This
// component is a standalone component
// and we could have passed an entire
// object such as {id: ..., name, ..., description, ...}
// since we use all the datas (and when we use all the data it's
// a better approach since we don't want to write dozens of propTypes)
// but let's do that for the example's sake
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
}
render: function() {
return (
<li>
<p>{this.props.id}</p>
<p>{this.props.name}</p>
<p>{this.props.description}</p>
</li>
)
}
});
components/BarDetail.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
var BarActionCreators = require('../actions/BarActionCreators');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
},
handleSubmit: function(event) {
//Since we keep the Bar data up to date with user input
//we can simply save the actual object in Store.
//If the user goes back without saving, we could display a
//"Warning : item not saved"
BarActionCreators.save(this.props.id);
},
handleChange: function(event) {
BarActionCreators.rehydrate(
this.props.id,
event.target.name, //the field we want to rehydrate
event.target.value //the updated value
);
},
render: function() {
return (
<form onSubmit={this.handleSumit}>
<input
type="text"
name="name"
value={this.props.name}
onChange={this.handleChange}/>
<textarea
name="description"
value={this.props.description}
onChange={this.handleChange}/>
<input
type="submit"
defaultValue="Submit"/>
</form>
)
},
});
components/FooList.react.js (コントローラー ビュー コンポーネント)
var React = require('react/addons');
var FooStore = require('../stores/FooStore');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
fooList: FooStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
FooStore.addChangeListener(this._onChange);
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
FooStore.removeChangeListener(this._onChange);
BarStore.removeChangeListener(this._onChange);
},
render: function() {
if (this.state.fooList.size == 0) {
return <p>Loading...</p>
}
return this.state.fooList.toJS().map(function (foo) {
<FooListItem
fooId={foo.get('id')}
fooBar={foo.getBar()}
fooBaz={foo.get('baz')}/>
});
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
components/FooListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Bar = require('../stores/BarStore').Bar;
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
fooId: React.PropTypes.number.isRequired,
fooBar: React.PropTypes.instanceOf(Bar).isRequired,
fooBaz: React.PropTypes.string.isRequired
}
render: function() {
//we could (should) use a component here but this answer is already too long...
var bar = <p>Loading...</p>;
if (bar.isReady()) {
bar = (
<div>
<p>{bar.get('name')}</p>
<p>{bar.get('description')}</p>
</div>
);
}
return (
<div>
<p>{this.props.fooId}</p>
<p>{this.props.fooBaz}</p>
{bar}
</div>
)
},
});
for のループ全体を見てみましょうFooList
:
状態 1:
FooList
ユーザーは、コントローラー ビュー コンポーネントを介して Foos を一覧表示するページ /foos/ にアクセスします。
FooList
コントローラー ビュー コンポーネントの呼び出しFooStore.getAll()
_foos
マップは空なFooStore
のでFooStore
、経由でリクエストを実行しますFooAPI.getAll()
FooList
コントローラー ビュー コンポーネントは、state.fooList.size == 0
.
リストの実際の外観は次のとおりです。
++++++++++++++++++++++++
+ +
+ "loading..." +
+ +
++++++++++++++++++++++++
FooAPI.getAll()
FooActionCreators.receiveAllSuccess
リクエストが解決され、アクションがトリガーされます
FooStore
このアクションを受け取り、内部状態を更新し、変更を発行します。
状態 2:
FooList
controller-view コンポーネントは変更イベントを受け取り、その状態を更新してからリストを取得します。FooStore
this.state.fooList.size
== 0
リストが実際にそれ自体をレンダリングすることはできなくなりました (未加工のオブジェクトへのマッピングをまだ正しく処理していないtoJS()
ため、未加工の JavaScript オブジェクトを明示的に取得するために使用していることに注意してください)。React
FooListItem
必要な小道具をコンポーネントに渡しています。
- 電話をかけることで、レコードを取り戻したいことを に
foo.getBar()
伝えます。FooStore
Bar
getBar()
Foo
レコードのメソッドはBar
、BarStore
BarStore
Bar
はキャッシュにこのレコードを持っていないため、それを取得するために_bars
リクエストをトリガーします。BarAPI
- コントローラービューコンポーネントのすべて
Foo
で同じことが起こりますthis.sate.fooList
FooList
- ページは次のようになります。
++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 バー: +
+ 「読み込んでいます...」 +
+ +
++++++++++++++++++++++++
BarAPI.get(2)
- ここで、(Foo2 によって要求された) が ( Foo1 によって要求された) 前に解決されるとしましょうBarAPI.get(1)
。非同期なので、完全にもっともらしいです。- BarActionCreators BarStore` のBarAPI
トリガーは、内部ストアを更新することでこのアクションに応答し、変更を発行します。それが今の楽しい部分です...BAR_RECEIVED_SUCCESS' action via the
.
- The
状態 3:
FooList
コントローラー ビュー コンポーネントは、その状態を更新することで変更に対応しますBarStore
。
render
メソッドが呼び出されます
- この呼び出しは、から実際のレコードを
foo.getBar()
取得するようになりました。このレコードは効果的に取得されているため、 は古い props を現在の props と比較し、オブジェクトが変更されたことを判断します! ビンゴ、コンポーネントを再レンダリングできます (ここでのより良いアプローチは、別の FooListBarDetail コンポーネントを作成して、このコンポーネントのみを再レンダリングできるようにすることです。ここでは、変更されていない Foo の詳細も再レンダリングしますが、簡単にそうしましょう)。Bar
BarStore
Bar
ImmutablePureRenderMixin
Bar
FooListItem
- ページは次のようになります。
++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 バー: +
+ 「読み込んでいます...」 +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 バー: +
+ 「バー名」 +
+ 「バーの説明」 +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 バー: +
+ 「読み込んでいます...」 +
+ +
++++++++++++++++++++++++
詳細でない部分 (アクションクリエーター、定数、ルーティングなど、フォーム付きコンポーネントの使用、POST など) から詳細を追加したい場合はBarListDetail
、コメントで教えてください:)。