(Remarque : j'ai utilisé la syntaxe ES6 en utilisant l'option JSX Harmony).
À titre d'exercice, j'ai écrit un exemple d'application Flux qui permet de parcourir les utilisateurs et les dépôts de Github.
Il est basé sur La réponse de fisherwebdev mais reflète également une approche que j'utilise pour normaliser les réponses aux API.
Je l'ai fait pour documenter quelques approches que j'ai essayées en apprenant Flux.
J'ai essayé de le garder proche du monde réel (pagination, pas de fausse API localStorage).
Il y a quelques éléments qui m'ont particulièrement intéressé :
Comment je classe les magasins
J'ai essayé d'éviter certaines des duplications que j'ai vues dans d'autres exemples de Flux, notamment dans les magasins. J'ai trouvé utile de diviser logiquement les magasins en trois catégories :
Magasins de contenu contient toutes les entités de l'application. Tout ce qui a un ID a besoin de son propre Content Store. Les composants qui rendent les éléments individuels demandent aux Content Stores les nouvelles données.
Les magasins de contenu récoltent leurs objets à partir de todo actions du serveur. Par exemple, UserStore
regarde dans action.response.entities.users
s'il existe indépendamment de dont l'action a été tirée. Il n'y a pas besoin d'un switch
. Normalizr permet d'aplatir facilement les réponses de l'API à ce format.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Liste des magasins garder la trace des ID des entités qui apparaissent dans une liste globale (par exemple, "feed", "vos notifications"). Dans ce projet, je n'ai pas de tels magasins, mais j'ai pensé que je devais les mentionner quand même. Ils gèrent la pagination.
Ils ne répondent normalement qu'à quelques actions (par ex. REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Magasins de listes indexées sont comme les magasins de liste, mais ils définissent une relation de type un à plusieurs. Par exemple, "les abonnés de l'utilisateur", "les astrologues du référentiel", "les référentiels de l'utilisateur". Ils gèrent également la pagination.
En outre, ils ne répondent normalement qu'à quelques actions (par ex. REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Dans la plupart des applications sociales, vous en aurez beaucoup et vous voulez être en mesure d'en créer rapidement une de plus.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Note : il ne s'agit pas de classes réelles ou autre ; c'est juste la façon dont j'aime penser aux magasins. J'ai cependant créé quelques aides.
createStore
Cette méthode vous donne le magasin le plus basique :
createStore(spec) {
var store = merge(EventEmitter.prototype, merge(spec, {
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
}));
_.each(store, function (val, key) {
if (_.isFunction(val)) {
store[key] = store[key].bind(store);
}
});
store.setMaxListeners(0);
return store;
}
Je l'utilise pour créer tous les magasins.
isInBag
, mergeIntoBag
Petites aides utiles pour les magasins de contenu.
isInBag(bag, id, fields) {
var item = bag[id];
if (!bag[id]) {
return false;
}
if (fields) {
return fields.every(field => item.hasOwnProperty(field));
} else {
return true;
}
},
mergeIntoBag(bag, entities, transform) {
if (!transform) {
transform = (x) => x;
}
for (var key in entities) {
if (!entities.hasOwnProperty(key)) {
continue;
}
if (!bag.hasOwnProperty(key)) {
bag[key] = transform(entities[key]);
} else if (!shallowEqual(bag[key], entities[key])) {
bag[key] = transform(merge(bag[key], entities[key]));
}
}
}
Stocke l'état de la pagination et applique certaines assertions (ne peut pas récupérer la page pendant la récupération, etc).
class PaginatedList {
constructor(ids) {
this._ids = ids || [];
this._pageCount = 0;
this._nextPageUrl = null;
this._isExpectingPage = false;
}
getIds() {
return this._ids;
}
getPageCount() {
return this._pageCount;
}
isExpectingPage() {
return this._isExpectingPage;
}
getNextPageUrl() {
return this._nextPageUrl;
}
isLastPage() {
return this.getNextPageUrl() === null && this.getPageCount() > 0;
}
prepend(id) {
this._ids = _.union([id], this._ids);
}
remove(id) {
this._ids = _.without(this._ids, id);
}
expectPage() {
invariant(!this._isExpectingPage, 'Cannot call expectPage twice without prior cancelPage or receivePage call.');
this._isExpectingPage = true;
}
cancelPage() {
invariant(this._isExpectingPage, 'Cannot call cancelPage without prior expectPage call.');
this._isExpectingPage = false;
}
receivePage(newIds, nextPageUrl) {
invariant(this._isExpectingPage, 'Cannot call receivePage without prior expectPage call.');
if (newIds.length) {
this._ids = _.union(this._ids, newIds);
}
this._isExpectingPage = false;
this._nextPageUrl = nextPageUrl || null;
this._pageCount++;
}
}
createListStore
, createIndexedListStore
, createListActionHandler
Rend la création de magasins de listes indexées aussi simple que possible en fournissant des méthodes passe-partout et une gestion des actions :
var PROXIED_PAGINATED_LIST_METHODS = [
'getIds', 'getPageCount', 'getNextPageUrl',
'isExpectingPage', 'isLastPage'
];
function createListStoreSpec({ getList, callListMethod }) {
var spec = {
getList: getList
};
PROXIED_PAGINATED_LIST_METHODS.forEach(method => {
spec[method] = function (...args) {
return callListMethod(method, args);
};
});
return spec;
}
/**
* Creates a simple paginated store that represents a global list (e.g. feed).
*/
function createListStore(spec) {
var list = new PaginatedList();
function getList() {
return list;
}
function callListMethod(method, args) {
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates an indexed paginated store that represents a one-many relationship
* (e.g. user's posts). Expects foreign key ID to be passed as first parameter
* to store methods.
*/
function createIndexedListStore(spec) {
var lists = {};
function getList(id) {
if (!lists[id]) {
lists[id] = new PaginatedList();
}
return lists[id];
}
function callListMethod(method, args) {
var id = args.shift();
if (typeof id === 'undefined') {
throw new Error('Indexed pagination store methods expect ID as first parameter.');
}
var list = getList(id);
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates a handler that responds to list store pagination actions.
*/
function createListActionHandler(actions) {
var {
request: requestAction,
error: errorAction,
success: successAction,
preload: preloadAction
} = actions;
invariant(requestAction, 'Pass a valid request action.');
invariant(errorAction, 'Pass a valid error action.');
invariant(successAction, 'Pass a valid success action.');
return function (action, list, emitChange) {
switch (action.type) {
case requestAction:
list.expectPage();
emitChange();
break;
case errorAction:
list.cancelPage();
emitChange();
break;
case successAction:
list.receivePage(
action.response.result,
action.response.nextPageUrl
);
emitChange();
break;
}
};
}
var PaginatedStoreUtils = {
createListStore: createListStore,
createIndexedListStore: createIndexedListStore,
createListActionHandler: createListActionHandler
};
Un mixin qui permet aux composants de s'accorder aux magasins qui les intéressent, par exemple mixins: [createStoreMixin(UserStore)]
.
function createStoreMixin(...stores) {
var StoreMixin = {
getInitialState() {
return this.getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(this.getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStores(this.props));
}
}
};
return StoreMixin;
}