Estou reescrevendo meu aplicativo para usar o Flux e tenho um problema com a recuperação de dados do Stores. Tenho muitos componentes e eles se aninham muito. Alguns deles são grandes ( Article
), alguns são pequenos e simples ( UserAvatar
, UserLink
).
Tenho lutado para saber onde na hierarquia de componentes devo ler os dados do Stores.
Tentei duas abordagens extremas, nenhuma das quais gostei muito:
Todos os componentes da entidade leem seus próprios dados
Cada componente que precisa de alguns dados do Store recebe apenas o ID da entidade e recupera a entidade por conta própria.
Por exemplo, Article
é passado articleId
, UserAvatar
e UserLink
são passados userId
.
Essa abordagem tem várias desvantagens significativas (discutidas no exemplo de código).
var Article = React.createClass({
mixins: [createStoreMixin(ArticleStore)],
propTypes: {
articleId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
article: ArticleStore.get(this.props.articleId);
}
},
render() {
var article = this.state.article,
userId = article.userId;
return (
<div>
<UserLink userId={userId}>
<UserAvatar userId={userId} />
</UserLink>
<h1>{article.title}</h1>
<p>{article.text}</p>
<p>Read more by <UserLink userId={userId} />.</p>
</div>
)
}
});
var UserAvatar = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<img src={user.thumbnailUrl} />
)
}
});
var UserLink = React.createClass({
mixins: [createStoreMixin(UserStore)],
propTypes: {
userId: PropTypes.number.isRequired
},
getStateFromStores() {
return {
user: UserStore.get(this.props.userId);
}
},
render() {
var user = this.state.user;
return (
<Link to='user' params={{ userId: this.props.userId }}>
{this.props.children || user.name}
</Link>
)
}
});
As desvantagens dessa abordagem:
- É frustrante ter componentes 100s potencialmente assinando lojas;
- É difícil controlar como os dados são atualizados e em que ordem, porque cada componente recupera seus dados independentemente;
- Mesmo que você já tenha uma entidade no estado, você é forçado a passar seu ID para os filhos, que irão recuperá-lo novamente (ou então quebrar a consistência).
Todos os dados são lidos uma vez no nível superior e passados para os componentes
Quando me cansei de rastrear bugs, tentei colocar todos os dados recuperados no nível superior. Isso, entretanto, provou ser impossível porque para algumas entidades eu tenho vários níveis de aninhamento.
Por exemplo:
- A
Category
contémUserAvatar
s de pessoas que contribuem para essa categoria; - Um
Article
pode ter váriosCategory
s.
Portanto, se eu quisesse recuperar todos os dados do Stores no nível de um Article
, precisaria:
- Recuperar artigo de
ArticleStore
; - Recupere todas as categorias de artigos de
CategoryStore
; - Recupere separadamente os colaboradores de cada categoria
UserStore
; - De alguma forma, passe todos esses dados para os componentes.
Ainda mais frustrante, sempre que preciso de uma entidade profundamente aninhada, preciso adicionar código a cada nível de aninhamento para transmiti-lo adicionalmente.
Resumindo
Ambas as abordagens parecem falhas. Como posso resolver este problema de forma mais elegante?
Meus objetivos:
As lojas não deveriam ter um número absurdo de assinantes. É estúpido para cada um
UserLink
ouvirUserStore
se os componentes pais já fazem isso.Se o componente pai recuperou algum objeto da loja (por exemplo
user
), não quero que nenhum componente aninhado precise buscá-lo novamente. Devo ser capaz de passar pelos adereços.Eu não deveria ter que buscar todas as entidades (incluindo relacionamentos) no nível superior, porque isso complicaria adicionar ou remover relacionamentos. Não quero introduzir novos adereços em todos os níveis de aninhamento cada vez que uma entidade aninhada obtém um novo relacionamento (por exemplo, a categoria obtém a
curator
).
fonte
Respostas:
A maioria das pessoas começa ouvindo as lojas relevantes em um componente de visualização do controlador próximo ao topo da hierarquia.
Mais tarde, quando parecer que muitos adereços irrelevantes estão sendo transmitidos pela hierarquia para algum componente profundamente aninhado, algumas pessoas decidirão que é uma boa ideia deixar um componente mais profundo escutar as mudanças nas lojas. Isso oferece um encapsulamento melhor do domínio do problema de que trata este ramo mais profundo da árvore de componentes. Existem bons argumentos para fazer isso com cautela.
No entanto, prefiro ouvir sempre no topo e simplesmente passar todos os dados. Às vezes, vou até pegar todo o estado da loja e passá-lo para baixo na hierarquia como um único objeto, e farei isso para várias lojas. Portanto, eu teria um prop para o
ArticleStore
estado de e outro para oUserStore
estado de, etc. Acho que evitar visualizações de controlador profundamente aninhadas mantém um ponto de entrada único para os dados e unifica o fluxo de dados. Caso contrário, tenho várias fontes de dados e isso pode se tornar difícil de depurar.A verificação de tipo é mais difícil com esta estratégia, mas você pode configurar uma "forma", ou modelo de tipo, para o objeto grande como objeto com os PropTypes do React. Veja: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -validação
Observe que você pode querer colocar a lógica de associação de dados entre as lojas nas próprias lojas. Portanto, o seu
ArticleStore
poderwaitFor()
oUserStore
e, incluir os usuários relevantes com cadaArticle
registro que fornece atravésgetArticles()
. Fazer isso em suas visualizações soa como empurrar a lógica para a camada de visualização, que é uma prática que você deve evitar sempre que possível.Você também pode ficar tentado a usar
transferPropsTo()
, e muitas pessoas gostam de fazer isso, mas prefiro manter tudo explícito para facilitar a leitura e, portanto, a manutenção.FWIW, meu entendimento é que David Nolen adota uma abordagem semelhante com seu framework Om (que é um tanto compatível com Flux ) com um único ponto de entrada de dados no nó raiz - o equivalente no Flux seria ter apenas uma visualização do controlador ouvindo todas as lojas. Isso se torna eficiente usando
shouldComponentUpdate()
estruturas de dados imutáveis que podem ser comparadas por referência, com ===. Para estruturas de dados imutáveis, verifique o mori de David ou o immutable-js do Facebook . Meu conhecimento limitado de Om vem principalmente de The Future of JavaScript MVC Frameworksfonte
A abordagem a que cheguei é fazer com que cada componente receba seus dados (não IDs) como um suporte. Se algum componente aninhado precisar de uma entidade relacionada, cabe ao componente pai recuperá-la.
Em nosso exemplo,
Article
deve haver umarticle
suporte que é um objeto (presumivelmente recuperado porArticleList
ouArticlePage
).Porque
Article
também quer renderizarUserLink
eUserAvatar
para autor do artigo, vai se inscreverUserStore
e manterauthor: UserStore.get(article.authorId)
no seu estado. Em seguida, será renderizadoUserLink
eUserAvatar
com issothis.state.author
. Se eles quiserem passar adiante, eles podem. Nenhum componente filho precisará recuperar este usuário novamente.Reiterar:
Isso resolve meu problema muito bem. Exemplo de código reescrito para usar esta abordagem:
Isso mantém os componentes mais internos estúpidos, mas não nos força a complicar muito os componentes de nível superior.
fonte
Minha solução é muito mais simples. Cada componente que possui seu próprio estado pode falar e ouvir lojas. Esses são componentes muito parecidos com controladores. Componentes aninhados mais profundos que não mantêm o estado, mas apenas renderizam coisas, não são permitidos. Eles só recebem adereços para renderização pura, muito semelhante à vista.
Dessa forma, tudo flui de componentes com estado para componentes sem estado. Manter a contagem de statefuls baixa.
No seu caso, o artigo seria stateful e, portanto, fala com as lojas e o UserLink etc. seria apenas renderizado para receber o artigo.user como prop.
fonte
Os problemas descritos em suas 2 filosofias são comuns a qualquer aplicativo de página única.
Eles são discutidos brevemente neste vídeo: https://www.youtube.com/watch?v=IrgHurBjQbg and Relay ( https://facebook.github.io/relay ) foi desenvolvido pelo Facebook para superar a desvantagem que você descreve.
A abordagem do Relay é muito centrada nos dados. É uma resposta à pergunta "Como obtenho apenas os dados necessários para cada componente desta visualização em uma consulta ao servidor?" E, ao mesmo tempo, o Relay garante que você tenha pouco acoplamento no código quando um componente é usado em várias visualizações.
Se Relay não for uma opção , "Todos os componentes da entidade leem seus próprios dados" parece uma abordagem melhor para a situação que você descreve. Acho que o equívoco no Flux é o que é uma loja. O conceito de loja não existe para ser o lugar onde um modelo ou uma coleção de objetos são guardados. Os armazenamentos são locais temporários onde seu aplicativo coloca os dados antes da exibição ser renderizada. O verdadeiro motivo de sua existência é para resolver o problema de dependências entre os dados que vão para diferentes armazenamentos.
O que o Flux não especifica é como uma loja se relaciona com o conceito de modelos e coleção de objetos (a la Backbone). Nesse sentido, algumas pessoas estão realmente fazendo um armazenamento de fluxo um lugar onde colocar uma coleção de objetos de um tipo específico que não é nivelado durante todo o tempo que o usuário mantém o navegador aberto, mas, pelo que entendi o fluxo, não é isso que uma loja deve ser.
A solução é ter outra camada onde as entidades necessárias para renderizar sua visualização (e potencialmente mais) são armazenadas e mantidas atualizadas. Se você usar essa camada que abstrai modelos e coleções, não será um problema se os subcomponentes tiverem que consultar novamente para obter seus próprios dados.
fonte