(Nota: he usado la sintaxis ES6 usando la opción JSX Harmony).
Como ejercicio, escribí una aplicación Flux de muestra que permite navegar Github users
y reposicionar.
Se basa en la respuesta de fisherwebdev, pero también refleja un enfoque que uso para normalizar las respuestas API.
Logré documentar algunos enfoques que probé mientras aprendía Flux.
Traté de mantenerlo cerca del mundo real (paginación, sin API de almacenamiento local falsas).
Aquí hay algunos bits en los que estaba especialmente interesado:
Cómo clasifico las tiendas
Traté de evitar parte de la duplicación que he visto en otro ejemplo de Flux, específicamente en las tiendas. Me pareció útil dividir lógicamente las tiendas en tres categorías:
Las tiendas de contenido contienen todas las entidades de la aplicación. Todo lo que tiene una ID necesita su propio Content Store. Los componentes que representan elementos individuales solicitan a los Almacenes de contenido los datos nuevos.
Los almacenes de contenido cosechan sus objetos de todas las acciones del servidor. Por ejemplo, UserStore
analizaaction.response.entities.users
si existe independientemente de la acción que se active. No hay necesidad de a switch
. Normalizr facilita aplanar cualquier respuesta API a este formato.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Los almacenes de listas realizan un seguimiento de los ID de las entidades que aparecen en alguna lista global (por ejemplo, "feed", "sus notificaciones"). En este proyecto, no tengo tales tiendas, pero pensé en mencionarlas de todos modos. Manejan la paginación.
Normalmente responden a unas pocas acciones (p REQUEST_FEED
. Ej . REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Los almacenes de listas indexadas son como los almacenes de listas, pero definen una relación de uno a muchos. Por ejemplo, "suscriptores del usuario", "observadores de estrellas del repositorio", "repositorios del usuario". También manejan la paginación.
También responden normalmente a unas pocas acciones (por ejemplo REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
En la mayoría de las aplicaciones sociales, tendrá muchas de estas y desea poder crear rápidamente una más.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Nota: estas no son clases reales o algo así; es justo como me gusta pensar en las tiendas. Aunque hice algunos ayudantes.
createStore
Este método le brinda la Tienda más básica:
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;
}
Lo uso para crear todas las tiendas.
isInBag
, mergeIntoBag
Pequeños ayudantes útiles para Content Stores.
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]));
}
}
}
Almacena el estado de paginación y aplica ciertas afirmaciones (no se puede recuperar la página mientras se recupera, 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
Hace que la creación de almacenes de listas indexadas sea lo más simple posible al proporcionar métodos repetitivos y manejo de acciones:
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 que permite que los componentes sintonicen las tiendas que les interesan, por ejemplo 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
, con todos los usuarios relevantes en él. Y cada usuario tendría un par de banderas booleanas que describen la relación con el perfil de usuario actual. Algo como{ follower: true, followed: false }
, por ejemplo. Los métodosgetFolloweds()
ygetFollowers()
recuperarían los diferentes conjuntos de usuarios que necesita para la interfaz de usuario.