A quale livello di annidamento i componenti dovrebbero leggere le entità da Stores in Flux?


89

Sto riscrivendo la mia app per utilizzare Flux e ho un problema con il recupero dei dati dagli Store. Ho molti componenti e si annidano molto. Alcuni di loro sono grandi ( Article), altri piccoli e semplici ( UserAvatar, UserLink).

Ho lottato con dove nella gerarchia dei componenti dovrei leggere i dati dai negozi.
Ho provato due approcci estremi, nessuno dei quali mi è piaciuto abbastanza:

Tutti i componenti dell'entità leggono i propri dati

Ogni componente che necessita di alcuni dati da Store riceve solo l'ID entità e recupera l'entità da sola.
Ad esempio, Articleè passato articleId, UserAvatare UserLinksono passati userId.

Questo approccio ha diversi svantaggi significativi (discussi nell'esempio di codice).

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>
    )
  }
});

Aspetti negativi di questo approccio:

  • È frustrante avere 100 componenti potenzialmente iscritti a Stores;
  • È difficile tenere traccia di come i dati vengono aggiornati e in quale ordine perché ogni componente recupera i propri dati in modo indipendente;
  • Anche se potresti già avere un'entità nello stato, sei costretto a passare il suo ID ai bambini, che lo recupereranno di nuovo (oppure romperanno la coerenza).

Tutti i dati vengono letti una volta al livello superiore e passati ai componenti

Quando ero stanco di rintracciare i bug, ho provato a mettere tutti i dati recuperati al livello più alto. Ciò, tuttavia, si è rivelato impossibile perché per alcune entità ho diversi livelli di nidificazione.

Per esempio:

  • A Categorycontiene UserAvatars di persone che contribuiscono a quella categoria;
  • An Articlepuò avere diversi messaggi Category.

Pertanto, se volessi recuperare tutti i dati dagli Store a livello di un Article, avrei bisogno di:

  • Recupera l'articolo da ArticleStore;
  • Recupera tutte le categorie di articoli da CategoryStore;
  • Recupera separatamente i contributori di ciascuna categoria da UserStore;
  • In qualche modo passa tutti quei dati ai componenti.

Ancora più frustrante, ogni volta che ho bisogno di un'entità profondamente annidata, avrei bisogno di aggiungere codice a ogni livello di annidamento per trasmetterlo ulteriormente.

Riassumendo

Entrambi gli approcci sembrano imperfetti. Come risolvo questo problema nel modo più elegante?

I miei obiettivi:

  • I negozi non dovrebbero avere un numero folle di abbonati. È stupido per ciascuno UserLinkascoltare UserStorese i componenti principali lo fanno già.

  • Se il componente padre ha recuperato un oggetto dall'archivio (ad esempio user), non voglio che alcun componente annidato debba recuperarlo di nuovo. Dovrei essere in grado di passarlo tramite oggetti di scena.

  • Non dovrei dover recuperare tutte le entità (comprese le relazioni) al livello superiore perché complicherebbe l'aggiunta o la rimozione delle relazioni. Non voglio introdurre nuovi oggetti di scena a tutti i livelli di nidificazione ogni volta che un'entità annidata ottiene una nuova relazione (ad esempio, la categoria ottiene una curator).


1
Grazie per aver postato questo. Ho appena pubblicato una domanda molto simile qui: groups.google.com/forum/… Tutto quello che ho visto ha avuto a che fare con strutture dati piatte piuttosto che con dati associati. Sono sorpreso di non vedere alcuna best practice su questo.
uberllama

Risposte:


37

La maggior parte delle persone inizia ascoltando i negozi pertinenti in un componente di visualizzazione del controller nella parte superiore della gerarchia.

Più tardi, quando sembra che molti oggetti di scena irrilevanti vengano trasmessi attraverso la gerarchia a un componente profondamente annidato, alcune persone decideranno che è una buona idea lasciare che un componente più profondo ascolti i cambiamenti nei negozi. Ciò offre un migliore incapsulamento del dominio problematico di cui tratta questo ramo più profondo dell'albero dei componenti. Ci sono buoni argomenti da fare per farlo con giudizio.

Tuttavia, preferisco ascoltare sempre all'inizio e trasmettere semplicemente tutti i dati. A volte prendo anche l'intero stato del negozio e lo trasmetto attraverso la gerarchia come un singolo oggetto, e lo farò per più negozi. Quindi avrei un supporto per lo ArticleStorestato di, e un altro per lo UserStorestato di, ecc. Trovo che evitare viste controller profondamente annidate mantenga un punto di ingresso singolare per i dati e unifica il flusso di dati. In caso contrario, ho più fonti di dati e questo può diventare difficile da eseguire il debug.

Il controllo del tipo è più difficile con questa strategia, ma puoi impostare una "forma", o modello di tipo, per l'oggetto di grandi dimensioni come oggetto con PropTypes di React. Vedi: https://github.com/facebook/react/blob/master/src/core/ReactPropTypes.js#L76-L91 http://facebook.github.io/react/docs/reusable-components.html#prop -validazione

Tieni presente che potresti voler inserire la logica di associazione dei dati tra i negozi nei negozi stessi. Quindi la tua ArticleStoreforza waitFor()la UserStore, e comprendono gli Utenti rilevanti con ogni Articlerecord di esso fornisce attraverso getArticles(). Fare questo nelle tue viste suona come inserire la logica nel livello della vista, che è una pratica che dovresti evitare quando possibile.

Potresti anche essere tentato di usarlo transferPropsTo(), ea molte persone piace farlo, ma preferisco mantenere tutto esplicito per la leggibilità e quindi la manutenibilità.

FWIW, la mia comprensione è che David Nolen adotta un approccio simile con il suo framework Om (che è in qualche modo compatibile con Flux ) con un singolo punto di ingresso dei dati sul nodo radice: l'equivalente in Flux sarebbe avere solo una visualizzazione del controller ascoltando tutti i negozi. Ciò è reso efficiente utilizzando shouldComponentUpdate()strutture di dati immutabili che possono essere confrontate per riferimento, con ===. Per strutture di dati immutabili, controlla il mori di David o immutable-js di Facebook . La mia conoscenza limitata di Om proviene principalmente da The Future of JavaScript MVC Frameworks


4
Grazie per una risposta dettagliata! Ho preso in considerazione la possibilità di trasmettere entrambe le intere istantanee dello stato dei negozi. Tuttavia questo potrebbe darmi un problema di prestazioni perché ogni aggiornamento di UserStore causerà il rendering di tutti gli articoli sullo schermo e PureRenderMixin non sarà in grado di dire quali articoli devono essere aggiornati e quali no. Per quanto riguarda il tuo secondo suggerimento, ci ho pensato anch'io, ma non riesco a vedere come posso mantenere l'uguaglianza dei riferimenti agli articoli se l'utente dell'articolo viene "iniettato" ad ogni chiamata getArticles (). Questo mi darebbe ancora potenzialmente problemi di prestazioni.
Dan Abramov

1
Capisco il tuo punto, ma ci sono soluzioni. Per prima cosa, potresti controllare shouldComponentUpdate () se un array di ID utente per un articolo è cambiato, il che fondamentalmente è solo il rollio manuale delle funzionalità e del comportamento che desideri veramente in PureRenderMixin in questo caso specifico.
fisherwebdev

3
Giusto, e comunque dovrei farlo solo quando sono certo che ci sono problemi di prestazioni, cosa che non dovrebbe essere il caso dei negozi che si aggiornano più volte al minuto al massimo. Grazie ancora!
Dan Abramov

8
A seconda del tipo di applicazione che stai costruendo, mantenere un singolo punto di ingresso farà male non appena inizierai ad avere più "widget" sullo schermo che non appartengono direttamente alla stessa radice. Se provi a farlo con forza in questo modo, quel nodo radice avrà troppe responsabilità. KISS and SOLID, anche se è lato client.
Rygu

37

L'approccio a cui sono arrivato è che ogni componente riceva i propri dati (non ID) come supporto. Se un componente nidificato necessita di un'entità correlata, spetta al componente padre recuperarlo.

Nel nostro esempio, Articledovrebbe avere un oggetto di articlescena che è un oggetto (presumibilmente recuperato da ArticleListo ArticlePage).

Perché Articlevuole anche rendere UserLinke UserAvatarper l'autore dell'articolo, si iscriverà UserStoree manterrà author: UserStore.get(article.authorId)nel suo stato. Quindi renderà UserLinke UserAvatarcon questo this.state.author. Se desiderano tramandarlo ulteriormente, possono. Nessun componente figlio dovrà recuperare di nuovo questo utente.

Reiterare:

  • Nessun componente riceve mai l'ID come sostegno; tutti i componenti ricevono i rispettivi oggetti.
  • Se i componenti figlio hanno bisogno di un'entità, è responsabilità del genitore recuperarla e passare come sostegno.

Questo risolve abbastanza bene il mio problema. Esempio di codice riscritto per utilizzare questo approccio:

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>
    )
  }
});

Ciò mantiene i componenti più interni stupidi ma non ci obbliga a complicare l'inferno con componenti di alto livello.


Solo per aggiungere una prospettiva in cima a questa risposta. Concettualmente, puoi sviluppare una nozione di proprietà, Data una raccolta dati finalizzare in alto la visualizzazione più chi possiede effettivamente i dati (quindi ascoltando il suo negozio). Alcuni esempi: 1. AuthData dovrebbe essere di proprietà di App Component, qualsiasi modifica in Auth dovrebbe essere propagata come oggetti di scena a tutti i figli da App Component. 2. ArticleData dovrebbe essere di proprietà di ArticlePage o Article List come suggerito in risposta. ora qualsiasi figlio di ArticleList può ricevere AuthData e / o ArticleData da ArticleList indipendentemente da quale componente abbia effettivamente recuperato quei dati.
Saumitra R. Bhave

2

La mia soluzione è molto più semplice. Ogni componente che ha il proprio stato può parlare e ascoltare i negozi. Questi sono componenti molto simili a controller. Non sono consentiti componenti annidati più profondi che non mantengono lo stato ma si limitano a eseguire il rendering. Ricevono solo oggetti di scena per il rendering puro, molto simile alla visualizzazione.

In questo modo tutto scorre dai componenti stateful ai componenti stateless. Mantenere basso il numero di stateful.

Nel tuo caso, l'articolo sarebbe stateful e quindi i colloqui con i negozi e UserLink ecc. Verrebbero visualizzati solo in modo da ricevere article.user come prop.


1
Suppongo che questo funzionerebbe se l'articolo avesse la proprietà dell'oggetto utente, ma nel mio caso ha solo userId (perché l'utente reale è memorizzato in UserStore). O mi manca il tuo punto? Condivideresti un esempio?
Dan Abramov

Avvia un recupero per l'utente nell'articolo. Oppure modifica il backend dell'API per rendere l'ID utente "espandibile" in modo da ottenere l'utente immediatamente. In ogni caso, mantieni le viste semplici dei componenti annidati più profondi e fai affidamento solo sugli oggetti di scena.
Rygu

2
Non posso permettermi di avviare il recupero nell'articolo perché deve essere visualizzato insieme. Per quanto riguarda le API espandibili, abbiamo utilizzato per averli, ma sono molto problematico per analizzare nei negozi. Ho persino scritto una libreria per appiattire le risposte proprio per questo motivo.
Dan Abramov

0

I problemi descritti nelle tue 2 filosofie sono comuni a qualsiasi applicazione a pagina singola.

Sono discussi brevemente in questo video: https://www.youtube.com/watch?v=IrgHurBjQbg e Relay ( https://facebook.github.io/relay ) è stato sviluppato da Facebook per superare il compromesso che descrivi.

L'approccio di Relay è molto incentrato sui dati. È una risposta alla domanda "Come ottengo solo i dati necessari per ogni componente in questa visualizzazione in una query al server?" E allo stesso tempo Relay si assicura di avere pochi accoppiamenti attraverso il codice quando un componente viene utilizzato in più viste.

Se Relay non è un'opzione , "Tutti i componenti dell'entità leggono i propri dati" mi sembra un approccio migliore per la situazione che descrivi. Penso che l'idea sbagliata in Flux sia cosa sia un negozio. Il concetto di negozio non esiste per essere il luogo in cui viene conservato un modello o una collezione di oggetti. Gli archivi sono luoghi temporanei in cui l'applicazione inserisce i dati prima che venga eseguito il rendering della vista. La vera ragione per cui esistono è risolvere il problema delle dipendenze tra i dati che vanno in archivi diversi.

Ciò che Flux non specifica è come un negozio si relaziona al concetto di modelli e collezione di oggetti (a la Backbone). In questo senso alcune persone stanno effettivamente facendo di un flux store un luogo in cui mettere la raccolta di oggetti di un tipo specifico che non è a filo per tutto il tempo l'utente tiene il browser aperto ma, a quanto ho capito flux, non è quello che un negozio dovrebbe essere.

La soluzione è avere un altro livello in cui sono archiviate e mantenute aggiornate le entità necessarie per il rendering della visualizzazione (e potenzialmente di più). Se questo livello astrarre modelli e raccolte, non è un problema se i sottocomponenti devono interrogare di nuovo per ottenere i propri dati.


1
Per quanto ne so, l'approccio di Dan mantenendo diversi tipi di oggetti e referenziandoli tramite ID ci consente di mantenere la cache di quegli oggetti e aggiornarli solo in un posto. Come si può ottenere ciò se, ad esempio, si hanno diversi articoli che fanno riferimento allo stesso utente e quindi il nome dell'utente viene aggiornato? L'unico modo è aggiornare tutti gli articoli con i nuovi dati utente. Questo è esattamente lo stesso problema che si verifica quando si sceglie tra documento e database relazionale per il proprio backend. Quali sono i tuoi pensieri al riguardo? Grazie.
Alex Ponomarev
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.