(Remarque: j'ai utilisé la syntaxe ES6 en utilisant l'option JSX Harmony.)
En guise d'exercice, j'ai écrit un exemple d'application Flux qui permet de parcourir Github users
et de déposer.
Il est basé sur la réponse de fisherwebdev mais reflète également une approche que j'utilise pour normaliser les réponses 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 fausses API localesStorage).
Il y a quelques éléments qui m'intéressaient particulièrement:
Comment je classe les magasins
J'ai essayé d'éviter une partie de la duplication que j'ai vue dans d'autres exemples de Flux, en particulier dans les magasins. J'ai trouvé utile de diviser logiquement les magasins en trois catégories:
Les magasins de contenu contiennent toutes les entités d'application. Tout ce qui a un identifiant a besoin de son propre Content Store. Les composants qui rendent des éléments individuels demandent aux Content Stores les nouvelles données.
Les magasins de contenu récupèrent leurs objets de toutes les actions du serveur. Par exemple, UserStore
regardeaction.response.entities.users
s'il existe quelle que soit l'action déclenchée. Il n'y a pas besoin d'un switch
. Normalizr facilite l'aplatissement des réponses d'API à ce format.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Les magasins de listes gardent une trace des identifiants des entités qui apparaissent dans une liste globale (par exemple «flux», «vos notifications»). Dans ce projet, je n'ai pas de tels magasins, mais je pensais les mentionner quand même. Ils gèrent la pagination.
Ils répondent normalement à quelques actions (par exemple REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Les magasins de listes indexées sont comme les magasins de listes, mais ils définissent une relation un-à-plusieurs. Par exemple, «abonnés de l'utilisateur», «astronomes du référentiel», «référentiels de l'utilisateur». Ils gèrent également la pagination.
Ils répondent aussi normalement que quelques actions (par exemple REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Dans la plupart des applications sociales, vous en aurez beaucoup et vous voulez pouvoir en créer rapidement une de plus.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Remarque: ce ne sont pas des classes réelles ou quelque chose du genre; c'est comme ça que j'aime penser aux magasins. J'ai cependant fait 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
Petits assistants 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 pagination et applique certaines assertions (impossible de récupérer la page lors de 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 standard et la 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 se connecter 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;
}
UserListStore
, avec tous les utilisateurs pertinents. Et chaque utilisateur aurait quelques indicateurs booléens décrivant la relation avec le profil utilisateur actuel. Quelque chose comme{ follower: true, followed: false }
, par exemple. Les méthodesgetFolloweds()
etgetFollowers()
récupéreraient les différents ensembles d'utilisateurs dont vous avez besoin pour l'interface utilisateur.