2015-08-27 17 views
12

Tworzę prostą aplikację CRUD z wykorzystaniem kontrolera strumieniowego Facebooka do obsługi tworzenia i edytowania postów dla angielskiej strony edukacyjnej. Ja obecnie mam do czynienia z API, które wygląda tak:Jak obsługiwać zagnieżdżone wywołania api w strumieniu

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

na wystawie i edytowanie stron dla aplikacji, chciałbym być w stanie pokazać wszystkich informacji na danym stanowisku, a także wszystkich to zdania i słowa zdań i szczegóły gramatyki na jednej stronie.

Kwestia, którą wybijam, polega na ustaleniu, jak zainicjować wszystkie asynchroniczne wywołania wymagane do zebrania wszystkich tych danych, a następnie skompilowanie potrzebnych danych ze wszystkich sklepów do jednego obiektu, który można ustawić jako stan w mój komponent najwyższego poziomu. Prąd (straszne) przykładem tego, co starałem się zrobić to w ten sposób:

Górny poziom 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; 
    }); 
    } 
} 

i tu jest moje PostsActions.js - pozostałych jednostek (zdań, gramatyki, słów) mają również podobne ActionCreators które działają w podobny sposób:

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

głównym problemem jest to, że dyspozytor Flux rzuca „nie może wywoływać w środku wysyłki” niezmiennego błędu przy SentencesActions.fetch nazywa w _handlePostsStoreChange zwrotnego becau se, że metoda SentencesActions wyzwala wywołanie przed zakończeniem wywołania zwrotnego wysyłki dla poprzedniej czynności.

Jestem świadomy, że mogę to naprawić, używając czegoś takiego jak _.defer lub setTimeout - jednak to naprawdę wydaje się, że po prostu łatam ten problem tutaj. Zastanawiałem się również nad zrobieniem całej tej logiki pobierania w samych akcjach, ale to też nie wydawało się poprawne i utrudniłoby obsługę błędów. Mam każdą z moich jednostek rozdzieloną na własne sklepy i działania - czy nie powinno być jakiegoś sposobu na poziomie komponentu, aby komponować to, czego potrzebuję, z poszczególnych sklepów danego podmiotu?

Otwarte na porady od każdego, kto dokonał czegoś podobnego!

+0

próbowałeś używać 'waitFor'? https://facebook.github.io/flux/docs/dispatcher.html – knowbody

+0

@knowbody Tak, spróbowałem użyć 'waitFor', ale tak naprawdę nie rozwiązało to problemu, ponieważ problem polega na tym, że druga akcja zostanie wysłana, zanim pierwsza będzie mogła zakończyć. Być może jednak moje zrozumienie 'waitFor' jest złe i po prostu nie używam go poprawnie? – joeellis

+1

@joeellis: czy możesz przygotować demo jsFiddle, demonstrując swoją sytuację problemową? –

Odpowiedz

5

Ale nie, nie ma hack, aby stworzyć akcję w środku wysyłki, a to jest zgodne z projektem. Działania nie powinny być rzeczami, które powodują zmianę. Mają być jak gazety, które informują o zastosowaniu zmiany w świecie zewnętrznym, a następnie aplikacja odpowiada na te wieści. Sklepy powodują same w sobie zmiany. Działania właśnie je informują.

także

Składniki nie powinny być podejmowaniu decyzji, kiedy pobrać dane. To jest logika aplikacji w warstwie widoku.

Bill Fisher, twórca Flux https://stackoverflow.com/a/26581808/4258088

komponentu decyduje, kiedy należy pobrać dane. To jest zła praktyka. Co zasadniczo powinieneś robić, to mieć swój komponent określający za pośrednictwem akcji, jakie dane mu potrzebne.

Sklep powinien odpowiadać za gromadzenie/pobieranie wszystkich potrzebnych danych.Ważne jest jednak, aby pamiętać, że po tym, jak sklep zażądał danych poprzez wywołanie API, odpowiedź powinna wywołać akcję, w przeciwieństwie do obsługi sklepu/zapisania odpowiedzi bezpośrednio.

Twoje sklepy mógłby wyglądać mniej więcej tak:

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

Wszystko, co musisz zrobić potem słucha sklepach. Również w zależności od tego, czy korzystasz z frameworku, a który chcesz emitować zdarzenie zmiany (ręcznie).

+1

Dzięki za to (i za wszystkie inne odpowiedzi). Wygląda na to, że źle działałem, zakładając, że inteligentny komponent najwyższego poziomu powinien działać jako typ kontrolera widoku, tzn. Powinien być w stanie działać wbrew wszystkim akcjom i magazynom, które musi skomponować/złożyć cały stan, w którym się znajduje. składniki dzieci muszą używać. Wygląda na to, że pomysł jest wadliwy, a sklep naprawdę powinien obsługiwać te żądania asynchroniczne. Szkoda, ponieważ wolałbym, aby działania były za nie odpowiedzialne, ale niestety, udowodniłem, że nie jest to możliwe przy obecnym paradygmacie strumienia. – joeellis

2

Myślę, że powinieneś mieć inny Store odzwierciedlający twoje modele danych i niektóre obiekty POJO odzwierciedlające instancje twojego obiektu. Tak więc twój obiekt Post będzie miał metody getSentence(), które z kolei będą wywoływać SentenceStore.get(id) itd. Wystarczy dodać metodę taką jak isReady() do obiektu Post zwracając numer true lub "fałsz zwinięty, wszystkie dane zostały pobrane lub nie.

Oto podstawowe wdrożenie używając 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

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

}); 

W ten sposób, trzeba tylko słuchać Powyższe zmiany sklepów w komponentu i napisać coś takiego (pseudo kod)

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

gdy użytkownik trafi na stronę, PostStore najpierw pobierze obiekt Post przez Ajax, a potrzebne dane zostaną załadowane przez SentenceStore i WordStore. Ponieważ ich słuchamy, a isReady() metoda zwraca tylko true, gdy zdania są gotowe, a isReady() metoda z zwraca tylko true, gdy wszystkie jej słowa zostały załadowane, nie masz nic do roboty :) Po prostu poczekaj, aż pokrętło do zostać zastąpiony przez twój post, gdy twoje dane są gotowe!

0

Nie wiem, jak wygląda twój stan aplikacji, ale dla mnie system, który zawsze działa najlepiej, gdy napotykam problemy z Fluxem, to przenieść więcej stanów i więcej logiki do sklepu. Próbowałem omijać to wiele razy i zawsze mnie to kąsało. Tak więc w najprostszym przykładzie wysłałbym jedną akcję, która obsługuje całą prośbę, jak również każdy stan, który się z nią zgadza. Tutaj jest bardzo prosty przykład, który powinien być stosunkowo Flux framework-agnostyk:

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

Następnie w swoim reagować składników użyć store.loading_state celu ustalenia, czy do renderowania jakąś załadunku turbinki, błąd, lub dane jako normalna.

Należy zauważyć, że w tym przypadku czynność nie przenosi niczego poza przekazanie obiektu opcji do metody składowania, która następnie obsługuje całą logikę i stan powiązane z wieloma żądaniami w jednym miejscu.

Daj mi znać, jeśli potrafię to wyjaśnić lepiej.

Powiązane problemy