Как обрабатывать вложенные вызовы api в потоке

Я создаю простое приложение CRUD с помощью Facebook Flux Dispatcher, чтобы обрабатывать создание и редактирование сообщений для английского учебного сайта. В настоящее время я занимаюсь 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)
    });
  }
}

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

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

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

Ответ 1

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

Кроме

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

Билл Фишер, создатель Flux fooobar.com/questions/34681/...

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

Магазин должен нести ответственность за накопление/выборку всех необходимых данных. Важно отметить, однако, что после того, как магазин запросил данные через вызов 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);
  }
}

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

Ответ 2

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

Вот базовая реализация, использующая 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, когда пост-предложения готовы, а метод isReady() Sentence возвращает true, когда все его слова загружены, вы нечего делать:) Просто подождите, пока счетчик будет заменен вашим сообщением, когда ваши данные будут готовы!

Ответ 3

Я не знаю, как обрабатывается ваше состояние приложения, но для меня система, которая всегда работает лучше всего, когда я сталкиваюсь с проблемами с 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';
        }
      }
    }
  }
}

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

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

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