![](/img/trans.png)
[英]How do I handle API calls with actions in the React.js Flux architecture and McFly?
[英]How to handle nested api calls in flux
我正在使用Facebook的Flux Dispatcher创建一个简单的CRUD应用程序来处理英语学习网站的帖子的创建和编辑。 我目前正在处理一个看起来像这样的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 - 其他实体(句子,语法,单词)也有类似的ActionCreator工作方式:
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)
});
}
}
主要问题是当在_handlePostsStoreChange
回调中调用SentencesActions.fetch
时,Flux调度程序抛出“无法在调度中间发送”不变错误,因为SentencesActions方法在前一个操作的调度回调完成之前触发调度。
我知道我可以通过使用像_.defer
或setTimeout
类的东西来解决这个问题 - 但是这真的感觉我只是在这里修补问题。 此外,我考虑在动作本身中执行所有这些获取逻辑,但这似乎也不正确,并且会使错误处理更加困难。 我将每个实体分离到自己的存储和操作中 - 组件级别是否应该有某种方式来构建每个实体各自存储所需的内容?
向任何已完成类似工作的人提供任何建议!
但不,没有黑客可以在调度过程中创建一个动作,这是设计的。 行动不应该是引起变化的事情。 它们应该像报纸一样通知应用程序外部世界的变化,然后应用程序响应该新闻。 商店本身会引起变化。 行动告诉他们。
也
组件不应该决定何时获取数据。 这是视图层中的应用程序逻辑。
Bill Fisher,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);
}
}
您需要做的就是收听商店。 还取决于您是否使用框架以及您需要发出哪个更改事件(手动)。
我认为您应该有不同的Store反映您的数据模型和一些反映您的对象实例的POJO对象。 因此,你的Post
对象将有一个getSentence()
方法,它们将调用SentenceStore.get(id)
等。你只需要将一个方法,如isReady()
到你的Post
对象返回true
或`false wether all数据是否已获取。
这是使用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
将首先通过Ajax检索Post对象,并且SentenceStore
和WordStore
将加载所需的数据。 由于我们正在监听它们并且Post
的isReady()
方法仅在post的句子准备就绪时返回true
,并且Sentence
isReady()
方法仅在所有单词都已加载时返回true
,否则无关:)只需等待当您的数据准备就绪时,您的帖子将替换微调器!
我不知道你的应用程序状态是如何处理的,但对我来说,当遇到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';
}
}
}
}
}
然后在您的React组件中使用store.loading_state
来确定是否正常呈现某种加载微调器,错误或数据。
请注意,在这种情况下,操作只会将选项对象传递给存储方法,然后存储方法会在一个位置处理与多个请求关联的所有逻辑和状态。
如果我能更好地解释这一点,请告诉我。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.