2015-08-27 15 views
12

Sto creando una semplice app CRUD utilizzando Flux Dispatcher di Facebook per gestire la creazione e la modifica di post per un sito di apprendimento in inglese. Attualmente occupo di un'API che assomiglia a questo:Come gestire le chiamate api annidate in flusso

/posts/:post_id 
/posts/:post_id/sentences 
/sentences/:sentence_id/words 
/sentences/:sentence_id/grammars 

nello show e modificare pagine per l'applicazione, mi piacerebbe essere in grado di mostrare tutte le informazioni per un determinato posto, così come tutti le frasi e le parole delle parole e i dettagli della grammatica si trovano tutti su un'unica pagina.

Il problema che sto colpendo è capire come avviare tutte le chiamate asincrone richieste per raccogliere tutti questi dati e quindi comporre i dati che ho bisogno da tutti i negozi in un singolo oggetto che posso impostare come stato in il mio componente di livello superiore. Una corrente (terribile) esempio di quello che ho cercato di fare è questo:

Il livello superiore PostsShowView:

class PostsShow extends React.Component { 
    componentWillMount() { 
    // this id is populated by react-router when the app hits the /posts/:id route 
    PostsActions.get({id: this.props.params.id}); 

    PostsStore.addChangeListener(this._handlePostsStoreChange); 
    SentencesStore.addChangeListener(this._handleSentencesStoreChange); 
    GrammarsStore.addChangeListener(this._handleGrammarsStoreChange); 
    WordsStore.addChangeListener(this._handleWordsStoreChange); 
    } 

    componentWillUnmount() { 
    PostsStore.removeChangeListener(this._handlePostsStoreChange); 
    SentencesStore.removeChangeListener(this._handleSentencesStoreChange); 
    GrammarsStore.removeChangeListener(this._handleGrammarsStoreChange); 
    WordsStore.removeChangeListener(this._handleWordsStoreChange); 
    } 

    _handlePostsStoreChange() { 
    let posts = PostsStore.getState().posts; 
    let post = posts[this.props.params.id]; 

    this.setState({post: post}); 

    SentencesActions.fetch({postId: post.id}); 
    } 

    _handleSentencesStoreChange() { 
    let sentences = SentencesStore.getState().sentences; 

    this.setState(function(state, sentences) { 
     state.post.sentences = sentences; 
    }); 

    sentences.forEach((sentence) => { 
     GrammarsActions.fetch({sentenceId: sentence.id}) 
     WordsActions.fetch({sentenceId: sentence.id}) 
    }) 
    } 

    _handleGrammarsStoreChange() { 
    let grammars = GrammarsStore.getState().grammars; 

    this.setState(function(state, grammars) { 
     state.post.grammars = grammars; 
    }); 
    } 

    _handleWordsStoreChange() { 
    let words = WordsStore.getState().words; 

    this.setState(function(state, words) { 
     state.post.words = words; 
    }); 
    } 
} 

Ed ecco la mia PostsActions.js - le altre entità (sentenze, grammatiche, parole) hanno anche ActionCreators simili che funzionano in modo simile:

let api = require('api'); 

class PostsActions { 
    get(params = {}) { 
    this._dispatcher.dispatch({ 
     actionType: AdminAppConstants.FETCHING_POST 
    }); 

    api.posts.fetch(params, (err, res) => { 
     let payload, post; 

     if (err) { 
     payload = { 
      actionType: AdminAppConstants.FETCH_POST_FAILURE 
     } 
     } 
     else { 
     post = res.body; 

     payload = { 
      actionType: AdminAppConstants.FETCH_POST_SUCCESS, 
      post: post 
     } 
     } 

     this._dispatcher.dispatch(payload) 
    }); 
    } 
} 

il problema principale è che il dispatcher Flux getta "Impossibile inviare nel bel mezzo di un dispaccio" errore invariata quando SentencesActions.fetch viene chiamato nel _handlePostsStoreChange callback becau Se il metodo SentencesActions attiva una distribuzione prima che la richiamata di dispatch per l'azione precedente sia terminata.

Sono consapevole che posso risolvere il problema utilizzando qualcosa come _.defer o setTimeout - tuttavia sembra davvero che io stia semplicemente correggendo il problema qui. Inoltre, ho considerato di fare tutta questa logica di recupero nelle azioni stesse, ma anche questo non sembrava corretto e renderebbe più difficile la gestione degli errori. Ho ciascuna delle mie entità separata nei propri negozi e azioni - non dovrebbe esserci qualche modo nel livello dei componenti per comporre quello che mi serve dai rispettivi negozi di ogni entità?

Aperto a qualsiasi consiglio da chiunque abbia realizzato qualcosa di simile!

+0

hai provato a utilizzare 'waitFor'? https://facebook.github.io/flux/docs/dispatcher.html – knowbody

+0

@knowbody Sì, ho provato a utilizzare 'waitFor', ma non sembra che risolva il problema, poiché il problema è che un la seconda azione viene inviata prima che il primo possa terminare. Tuttavia, forse la mia comprensione di 'waitFor' è sbagliata e non sto semplicemente usando correttamente? – joeellis

+1

@joeellis: è possibile mettere insieme una demo di jsFiddle per dimostrare la situazione del problema? –

risposta

5

Ma no, non c'è nessun trucco per creare un'azione nel bel mezzo di una spedizione, e questo è di progettazione. Le azioni non dovrebbero essere cose che causano un cambiamento. Dovrebbero essere come un giornale che informa l'applicazione di un cambiamento nel mondo esterno, e quindi l'applicazione risponde a quelle notizie. I negozi causano cambiamenti in se stessi. Le azioni li informano appena.

anche

componenti non dovrebbe essere decidere quando recuperare i dati. Questa è la logica dell'applicazione nel livello vista.

Bill Fisher, creatore di Flux https://stackoverflow.com/a/26581808/4258088

Il componente è decidere quando per recuperare i dati. Questa è una cattiva pratica. Quello che dovresti fare in pratica è che il tuo componente affermi tramite azioni quali dati ha bisogno.

Il negozio deve essere responsabile dell'accumulo/recupero di tutti i dati necessari.È importante notare, tuttavia, che dopo che lo store ha richiesto i dati tramite una chiamata API, la risposta deve attivare un'azione, contraria al negozio che gestisce/salva direttamente la risposta.

I suoi negozi potrebbero assomigliare a qualcosa di simile:

class Posts { 
    constructor() { 
    this.posts = []; 

    this.bindListeners({ 
     handlePostNeeded: PostsAction.POST_NEEDED, 
     handleNewPost: PostsAction.NEW_POST 
    }); 
    } 

    handlePostNeeded(id) { 
    if(postNotThereYet){ 
     api.posts.fetch(id, (err, res) => { 
     //Code 
     if(success){ 
      PostsAction.newPost(payLoad); 
     } 
     } 
    } 
    } 

    handleNewPost(post) { 
    //code that saves post 
    SentencesActions.needSentencesFor(post.id); 
    } 
} 

Tutto quello che devi fare, allora è l'ascolto della negozi. Dipende anche se si utilizza un framework e quale è necessario emettere l'evento change (manualmente).

+1

Grazie per questo (e per tutte le altre risposte). Sembra che il punto in cui stavo sbagliando stava assumendo che il componente smart di alto livello dovrebbe agire come un tipo di controller di visualizzazione, ovvero dovrebbe essere in grado di agire contro tutte le azioni e gli archivi necessari per comporre/assemblare tutto lo stato in cui si trova i componenti dei bambini devono essere utilizzati. Sembra che l'idea sia difettosa e lo store dovrebbe gestire davvero queste richieste asincrone. È un peccato, perché avrei preferito che le azioni fossero responsabili per loro, ma ahimè, penso di aver dimostrato che non è possibile con l'attuale paradigma del flusso. – joeellis

2

Penso che dovresti avere Store diverso che riflette i tuoi modelli di dati e alcuni oggetti POJO che riflettono le istanze del tuo oggetto. Pertanto, l'oggetto Post avrà un metodo getSentence() che a sua volta chiamerà lo SentenceStore.get(id) ecc. È sufficiente aggiungere un metodo come isReady() all'oggetto Post restituendo true o "false" se tutti i dati sono stati recuperati o meno.

Qui è un'implementazione di base utilizzando ImmutableJS:

PostSore.js

var _posts = Immutable.OrderedMap(); //key = post ID, value = Post 

class Post extends Immutable.Record({ 
    'id': undefined, 
    'sentences': Immutable.List(), 
}) { 

    getSentences() { 
     return SentenceStore.getByPost(this.id) 
    } 

    isReady() { 
     return this.getSentences().size > 0; 
    } 
} 

var PostStore = assign({}, EventEmitter.prototype, { 

    get: function(id) { 
     if (!_posts.has(id)) { //we de not have the post in cache 
      PostAPI.get(id); //fetch asynchronously the post 
      return new Post() //return an empty Post for now 
     } 
     return _post.get(id); 
    } 
}) 

SentenceStore.js

var _sentences = Immutable.OrderedMap(); //key = postID, value = sentence list 

class Sentence extends Immutable.Record({ 
    'id': undefined, 
    'post_id': undefined, 
    'words': Immutable.List(), 
}) { 

    getWords() { 
     return WordsStore.getBySentence(this.id) 
    } 

    isReady() { 
     return this.getWords().size > 0; 
    } 
} 

var SentenceStore = assign({}, EventEmitter.prototype, { 

    getByPost: function(postId) { 
     if (!_sentences.has(postId)) { //we de not have the sentences for this post yet 
      SentenceAPI.getByPost(postId); //fetch asynchronously the sentences for this post 
      return Immutable.List() //return an empty list for now 
     } 
     return _sentences.get(postId); 
    } 
}) 

var _setSentence = function(sentenceData) { 
    _sentences = _sentences.set(sentenceData.post_id, new Bar(sentenceData)); 
}; 

var _setSentences = function(sentenceList) { 
    sentenceList.forEach(function (sentenceData) { 
     _setSentence(sentenceData); 
    }); 
}; 

SentenceStore.dispatchToken = AppDispatcher.register(function(action) { 
    switch (action.type) 
    { 
     case ActionTypes.SENTENCES_LIST_RECEIVED: 
      _setSentences(action.sentences); 
      SentenceStore.emitChange(); 
      break; 
    } 
}); 

WordStore.js

0.123.
var _words = Immutable.OrderedMap(); //key = sentence id, value = list of words 

class Word extends Immutable.Record({ 
    'id': undefined, 
    'sentence_id': undefined, 
    'text': undefined, 
}) { 

    isReady() { 
     return this.id != undefined 
    } 
} 

var WordStore = assign({}, EventEmitter.prototype, { 

    getBySentence: function(sentenceId) { 
     if (!_words.has(sentenceId)) { //we de not have the words for this sentence yet 
      WordAPI.getBySentence(sentenceId); //fetch asynchronously the words for this sentence 
      return Immutable.List() //return an empty list for now 
     } 
     return _words.get(sentenceId); 
    } 

}); 

var _setWord = function(wordData) { 
    _words = _words.set(wordData.sentence_id, new Word(wordData)); 
}; 

var _setWords = function(wordList) { 
    wordList.forEach(function (wordData) { 
     _setWord(wordData); 
    }); 
}; 

WordStore.dispatchToken = AppDispatcher.register(function(action) { 
    switch (action.type) 
    { 
     case ActionTypes.WORDS_LIST_RECEIVED: 
      _setWords(action.words); 
      WordStore.emitChange(); 
      break; 
    } 

}); 

In questo modo, è solo bisogno di ascoltare i punti vendita indicati sopra cambiano nel componente e scrivere qualcosa di simile (pseudo codice)

YourComponents.jsx

getInitialState: 
    return {post: PostStore.get(your_post_id)} 

componentDidMount: 
    add listener to PostStore, SentenceStore and WordStore via this._onChange 

componentWillUnmount: 
    remove listener to PostStore, SentenceStore and WordStore 

render: 
    if this.state.post.isReady() //all data has been fetched 

    else 
     display a spinner   

_onChange: 
    this.setState({post. PostStore.get(your_post_id)}) 

Quando l'utente colpisce la pagina, PostStore recupererà prima l'oggetto Post tramite Ajax ei dati necessari verranno caricati da SentenceStore e WordStore. Dal momento che li stiamo ascoltando e il metodo isReady() di Post restituisce solo true quando le frasi del post sono pronte, e il metodo isReady() di Sentence restituisce solo true quando tutte le sue parole sono state caricate, non hai nulla da fare :) Aspetta solo che lo spinner essere sostituito dal tuo post quando i tuoi dati sono pronti!

0

Non so come viene gestito lo stato dell'applicazione, ma per me il sistema che funziona sempre meglio quando si verificano problemi con Flux è quello di spostare più stato e più logica per lo store. Ho cercato di aggirare questo un numero di volte, e finisce sempre per mordermi. Quindi, nell'esempio più semplice, invierò un'azione che gestisce l'intera richiesta, nonché qualsiasi stato che lo accompagni. Ecco un esempio molto semplice, che dovrebbe essere relativamente Flux quadro-agnostic:

var store = { 
    loading_state: 'idle', 
    thing_you_want_to_fetch_1: {}, 
    thing_you_want_to_fetch_2: {} 
} 

handleGetSomethingAsync(options) { 
    // do something with options 
    store.loading_state = 'loading' 
    request.get('/some/url', function(err, res) { 
    if (err) { 
     store.loading_state = 'error'; 
    } else { 
     store.thing_you_want_to_fetch_1 = res.body; 
     request.get('/some/other/url', function(error, response) { 
     if (error) { 
      store.loading_state = 'error'; 
     } else { 
      store.thing_you_want_to_fetch_2 = response.body; 
      store.loading_state = 'idle'; 
     } 
     } 
    } 
    } 
} 

Poi nelle vostre componenti reagiscono si utilizza il store.loading_state per determinare se eseguire il rendering un qualche tipo di carico filatore, un errore, o i dati come normale.

Si noti che in questo caso l'azione non fa altro che passare un oggetto opzioni a un metodo di archiviazione che quindi gestisce tutta la logica e lo stato associato alle richieste multiple in un unico punto.

Fammi sapere se posso spiegare meglio tutto ciò.

Problemi correlati