2015-08-27 2 views
12

Я создаю простое приложение CRUD с помощью Flux Dispatcher от Facebook, чтобы обрабатывать создание и редактирование сообщений для английского учебного сайта. В настоящее время я имею дело с API, который выглядит следующим образом:Как обрабатывать вложенные вызовы api в потоке

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

на шоу и редактирования страниц для приложения, я хотел бы быть в состоянии показать всю информацию для данной должности, а также все его предложения и слова предложений и грамматические подробности на одной странице.

Проблема, с которой я сталкиваюсь, - это выяснить, как инициировать все асинхронные вызовы, необходимые для сбора всех этих данных, а затем составлять нужные мне данные из всех хранилищ в один объект, который я могу установить как состояние в мой компонент верхнего уровня. Ток (ужасный) пример того, что я пытался сделать это:

Верхний уровень 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; 
    }); 
    } 
} 

А вот мой PostsActions.js - другие объекты (предложения, грамматик, слова) также имеют аналогичные ActionCreators, которые работают аналогичным образом:

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

Основная проблема заключается в том, что диспетчер потока бросает «не может отправить в середине отправки» инвариантной ошибка при SentencesActions.fetch называется в _handlePostsStoreChange обратного вызова becau se, что метод SentencesActions запускает отправку до завершения обратного вызова отправки для предыдущего действия.

Я знаю, что могу исправить это, используя что-то вроде _.defer или setTimeout. Однако мне кажется, что я просто исправляю проблему здесь. Кроме того, я считал, что я делаю всю эту выборку логики в самих действиях, но это тоже не правильно, и затруднит обработку ошибок. У меня есть все мои сущности, разделенные на свои собственные магазины и действия - не должно ли быть какой-то способ на уровне компонента составлять то, что мне нужно от соответствующих магазинов каждого объекта?

Открыт для любого совета любого, кто совершил нечто подобное!

+0

Вы пытались использовать 'waitFor'? https://facebook.github.io/flux/docs/dispatcher.html – knowbody

+0

@knowbody Да, я попытался использовать 'waitFor', но на самом деле это не похоже на проблему, поскольку проблема в том, что второе действие отправляется до первого завершения. Однако, может быть, мое понимание «waitFor» ошибочно, и я просто не использую его правильно? – joeellis

+1

@joeellis: возможно ли, чтобы вы собрали демо-версию jsFiddle, пожалуйста, продемонстрировав свою проблемную ситуацию? –

ответ

5

Но нет, нет никакого взлома, чтобы создать действие в середине отправки, и это по дизайну. Действия не должны быть вещами, которые вызывают изменения. Они должны быть похожи на газету, которая информирует о применении изменений во внешнем мире, а затем приложение реагирует на эти новости. Магазины создают изменения сами по себе. Действия просто информируют их.

Также

компоненты не должны решить, когда для извлечения данных. Это логика приложения на уровне представления.

Билл Фишер, создатель Flux https://stackoverflow.com/a/26581808/4258088

Ваш компонент решает, когда для извлечения данных. Это плохая практика. Что вы в основном должны делать, так это то, что ваш компонент указывает через действия какие данные он действительно нуждается.

Магазин должен нести ответственность за сбор/получение всех необходимых данных.Важно отметить, однако, что после того, как магазин запросил данные через вызов API, ответ должен вызвать действие, противоположное управлению хранилищем/сохранению ответа напрямую.

Ваши магазины могли бы выглядеть как-то вроде этого:

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

Все, что вам нужно сделать, а затем слушает магазины. Также зависит, используете ли вы фреймворк и какой из них нужно исправить событие изменения (вручную).

+1

Спасибо за это (и все остальные ответы). Похоже на то, что я ошибался, полагая, что интеллектуальный компонент верхнего уровня должен действовать как тип контроллера вида, то есть он должен иметь возможность действовать против всех действий и хранилищ, которые должны составлять/собирать все состояние, необходимо использовать компоненты для детей. Кажется, что идея ошибочна, и магазин действительно должен обрабатывать эти асинхронные запросы. Это позор, хотя, скорее всего, было бы скорее, если бы действия были ответственны за них, но, увы, я думаю, что доказал, что это невозможно с нынешней парадигмой потока. – joeellis

2

Я думаю, что у вас должен быть другой магазин, отражающий ваши модели данных и некоторые объекты POJO, отражающие экземпляры вашего объекта. Таким образом, ваш объект Post будет иметь методы getSentence(), которые по очереди будут называть SentenceStore.get(id) и т. Д. Вам просто нужно добавить метод, например isReady(), к вашему объекту Post, возвращающему true, или `false wether все данные были выбраны или нет.

Вот базовая реализация с использованием 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; 
    } 

}); 

Делая это, вам нужно только слушать выше магазинов изменить в компоненте и написать что-то вроде этого (псевдо-код)

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

Когда пользователь попадает на страницу, PostStore сначала получит объект Post через Ajax, и необходимые данные будут загружены SentenceStore и WordStore. Поскольку мы их слушаем, а метод isReady()Post возвращает true, когда предложения предложения готовы, а метод Sentence возвращает true, когда все его слова были загружены, вам нечего делать :) Просто подождите, пока счетчик быть замененным вашим сообщением, когда ваши данные будут готовы!

0

Я не знаю, как обрабатывается ваше состояние приложения, но для меня система, которая всегда работает лучше всего, когда я сталкиваюсь с проблемами с Flux, заключается в перемещении большего количества состояний и логики в хранилище. Я пытался обойти это несколько раз, и это всегда заканчивается кусать меня. Поэтому в самом простом примере я бы отправил одно действие, которое обрабатывает весь запрос, а также любое состояние, которое сопровождает его. Вот очень простой пример, который должен быть относительно Flux рамка агностика:

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

Тогда в ваших Реагировать компоненты вы используете store.loading_state, чтобы определить, следует ли оказывать какое-то погрузочная блесну, ошибку, или данные, как нормальный.

Обратите внимание, что в этом случае действие выполняет не более чем передачу объекта параметров до метода хранилища, который затем обрабатывает всю логику и состояние, связанные с несколькими запросами в одном месте.

Сообщите мне, если я могу объяснить все это лучше.

Смежные вопросы