(Nota: usei a sintaxe ES6 usando a opção JSX Harmony.)
Como exercício, escrevi um aplicativo Flux de amostra que permite navegar Github users
e reposicionar.
É baseado na resposta de fisherwebdev, mas também reflete uma abordagem que eu uso para normalizar as respostas da API.
Consegui documentar algumas abordagens que tentei enquanto aprendia o Flux.
Tentei mantê-lo próximo ao mundo real (paginação, nenhuma API localStorage falsa).
Existem alguns bits aqui nos quais eu estava especialmente interessado:
- Ele usa arquitetura Flux e reag-router ;
- Ele pode mostrar a página do usuário com informações conhecidas parciais e detalhes de carregamento em movimento;
- Ele suporta paginação para usuários e repositórios;
- Ele analisa as respostas JSON aninhadas do Github com normalizr ;
- Os armazenamentos de conteúdo não precisam conter um gigante
switch
com ações ;
- "Voltar" é imediato (porque todos os dados estão nas lojas).
Como classifico lojas
Tentei evitar parte da duplicação que já vi em outro exemplo do Flux, especificamente nas lojas. Achei útil dividir logicamente o Stores em três categorias:
Os armazenamentos de conteúdo mantêm todas as entidades de aplicativos. Tudo o que possui um ID precisa de seu próprio armazenamento de conteúdo. Os componentes que processam itens individuais solicitam novos dados aos armazenamentos de conteúdo.
Os armazenamentos de conteúdo coletam seus objetos de todas as ações do servidor. Por exemplo,UserStore
olhaaction.response.entities.users
, se existir , independentemente de qual a ação disparada. Não há necessidade de a switch
. O Normalizr facilita o nivelamento de qualquer resposta da API para esse formato.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Os armazenamentos de lista acompanham os IDs de entidades que aparecem em alguma lista global (por exemplo, "feed", "suas notificações"). Neste projeto, eu não tenho essas lojas, mas pensei em mencioná-las de qualquer maneira. Eles lidam com paginação.
Eles normalmente respondem a apenas algumas acções (por exemplo REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Os Armazéns de listas indexados são como os Armazéns de listas, mas definem o relacionamento um para muitos. Por exemplo, “assinantes de usuários”, “observadores de estrelas do repositório”, “repositórios de usuários”. Eles também lidam com paginação.
Eles também normalmente respondem a apenas algumas acções (por exemplo REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Na maioria dos aplicativos sociais, você terá muitos deles e deseja criar rapidamente mais um.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Nota: estas não são classes reais ou algo assim; é assim que eu gosto de pensar nas lojas. Eu fiz alguns ajudantes embora.
createStore
Este método fornece a loja mais 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;
}
Eu o uso para criar todas as lojas.
isInBag
, mergeIntoBag
Pequenos ajudantes úteis para armazenamentos de conteúdo.
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]));
}
}
}
Armazena o estado de paginação e aplica certas asserções (não é possível buscar a página durante a busca, 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
Facilita a criação dos armazenamentos de listas indexadas, fornecendo métodos padronizados e manipulação de ações:
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
};
Um mixin que permite que os componentes sintonizem-se nas lojas em que estão interessados, por exemplo 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
, com todos os usuários relevantes nela. E cada usuário teria alguns sinalizadores booleanos descrevendo o relacionamento com o perfil de usuário atual. Algo como{ follower: true, followed: false }
, por exemplo. Os métodosgetFolloweds()
egetFollowers()
recuperariam os diferentes conjuntos de usuários necessários para a interface do usuário.