Em que nível de aninhamento os componentes devem ler entidades de Stores in Flux?

89

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, UserAvatare UserLinksã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ê 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 Categorycontém UserAvatars de pessoas que contribuem para essa categoria;
  • Um Articlepode ter vários Categorys.

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 UserLinkouvir UserStorese 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).

Dan Abramov
fonte
1
Obrigado por postar isso. Acabei de postar uma pergunta muito semelhante aqui: groups.google.com/forum/… Tudo o que vi tratou de estruturas de dados simples, em vez de dados associados. Estou surpreso por não ver as melhores práticas sobre isso.
uberllama

Respostas:

37

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 ArticleStoreestado de e outro para o UserStoreestado 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 ArticleStorepoder waitFor()o UserStoree, incluir os usuários relevantes com cada Articleregistro que fornece através getArticles(). 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 Frameworks

Fisherwebdev
fonte
4
Obrigado por uma resposta detalhada! Considerei transmitir os dois instantâneos inteiros do estado das lojas. No entanto, isso pode me dar um problema de desempenho porque cada atualização do UserStore fará com que todos os artigos na tela sejam renderizados novamente, e o PureRenderMixin não será capaz de dizer quais artigos precisam ser atualizados e quais não. Quanto à sua segunda sugestão, também pensei sobre isso, mas não consigo ver como poderia manter a igualdade de referência do artigo se o usuário do artigo for "injetado" em cada chamada getArticles (). Isso potencialmente me causaria problemas de desempenho.
Dan Abramov
1
Eu vejo seu ponto, mas existem soluções. Por um lado, você pode verificar em shouldComponentUpdate () se uma matriz de IDs de usuário para um artigo mudou, o que é basicamente apenas um lançamento manual da funcionalidade e do comportamento que você realmente deseja no PureRenderMixin neste caso específico.
Fisherwebdev
3
Certo, e de qualquer maneira eu só precisa fazê-lo quando eu estou certo que são problemas perf que não deve ser o caso para as lojas que atualizar várias vezes por minuto máx. Obrigado de novo!
Dan Abramov
8
Dependendo do tipo de aplicativo que você está construindo, manter um único ponto de entrada vai doer assim que você começar a ter mais "widgets" na tela que não pertencem diretamente ao mesmo root. Se você forçosamente tentar fazer isso dessa forma, o nó raiz terá muita responsabilidade. KISS e SOLID, embora seja do lado do cliente.
Rygu
37

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, Articledeve haver um articlesuporte que é um objeto (presumivelmente recuperado por ArticleListou ArticlePage).

Porque Articletambém quer renderizar UserLinke UserAvatarpara autor do artigo, vai se inscrever UserStoree manter author: UserStore.get(article.authorId)no seu estado. Em seguida, será renderizado UserLinke UserAvatarcom isso this.state.author. Se eles quiserem passar adiante, eles podem. Nenhum componente filho precisará recuperar este usuário novamente.

Reiterar:

  • Nenhum componente recebe ID como suporte; todos os componentes recebem seus respectivos objetos.
  • Se os componentes filhos precisam de uma entidade, é responsabilidade do pai recuperá-la e passar como um objeto.

Isso resolve meu problema muito bem. Exemplo de código reescrito para usar esta abordagem:

var Article = React.createClass({
  mixins: [createStoreMixin(UserStore)],

  propTypes: {
    article: PropTypes.object.isRequired
  },

  getStateFromStores() {
    return {
      author: UserStore.get(this.props.article.authorId);
    }
  },

  render() {
    var article = this.props.article,
        author = this.state.author;

    return (
      <div>
        <UserLink user={author}>
          <UserAvatar user={author} />
        </UserLink>

        <h1>{article.title}</h1>
        <p>{article.text}</p>

        <p>Read more by <UserLink user={author} />.</p>
      </div>
    )
  }
});

var UserAvatar = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <img src={user.thumbnailUrl} />
    )
  }
});

var UserLink = React.createClass({
  propTypes: {
    user: PropTypes.object.isRequired
  },

  render() {
    var user = this.props.user;

    return (
      <Link to='user' params={{ userId: this.props.user.id }}>
        {this.props.children || user.name}
      </Link>
    )
  }
});

Isso mantém os componentes mais internos estúpidos, mas não nos força a complicar muito os componentes de nível superior.

Dan Abramov
fonte
Apenas para adicionar uma perspectiva sobre esta resposta. Conceitualmente, você pode desenvolver uma noção de propriedade, dada uma coleta de dados finalizar na visualização superior quem realmente possui os dados (portanto, ouvir seu armazenamento). Alguns exemplos: 1. AuthData deve ser propriedade do App Component - qualquer alteração no Auth deve ser propagada como suporte para todas as crianças pelo App Component. 2. ArticleData deve ser propriedade de ArticlePage ou Article List conforme sugerido na resposta. agora, qualquer filho de ArticleList pode receber AuthData e / ou ArticleData de ArticleList, independentemente de qual componente realmente obteve esses dados.
Saumitra R. Bhave
2

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.

Rygu
fonte
1
Suponho que isso funcionaria se o artigo tivesse propriedade de objeto de usuário, mas no meu caso ele só tem userId (porque o usuário real é armazenado em UserStore). Ou estou perdendo o seu ponto? Você compartilharia um exemplo?
Dan Abramov
Inicie uma busca para o usuário no artigo. Ou mude o back-end da API para tornar o ID do usuário "expansível" para que você obtenha o usuário imediatamente. De qualquer maneira, mantenha as visualizações simples dos componentes aninhados mais profundos e confie apenas em adereços.
Rygu
2
Não posso me dar ao luxo de iniciar a busca no artigo porque ele precisa ser renderizado junto. Quanto às APIs expansíveis, costumávamos tê-las, mas são muito problemáticas para analisar nas lojas. Eu até escrevi uma biblioteca para nivelar as respostas exatamente por esse motivo.
Dan Abramov
0

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.

Chris Cinelli
fonte
1
Até onde eu entendo, a abordagem de Dan mantendo diferentes tipos de objetos e referenciando-os por meio de ids nos permite manter o cache desses objetos e atualizá-los apenas em um lugar. Como isso pode ser feito se, digamos, você tiver vários artigos que fazem referência ao mesmo usuário e, em seguida, o nome do usuário for atualizado? A única maneira é atualizar todos os artigos com novos dados do usuário. Este é exatamente o mesmo problema que você tem ao escolher entre documento e banco de dados relacional para seu back-end. Quais são seus pensamentos sobre isso? Obrigado.
Alex Ponomarev