Loading all data when starting Flux SPA
… and check why 5600+ Rails engineers read also this
Loading all data when starting Flux SPA
Recently we’ve been working on an application which is a typical SPA which uses the Flux approach.
After some time we had a problem that besides our frontend being SPA, each time we clicked on link leading to some “page”, we’re loading data again, even if this data was loaded before. We’ve decided that the simplest solution would be to load all data in the beginning and display an animation when it’s loading.
In the future when necessity to hit refresh each time we want fresh data would become troublesome we could simply add Pusher-like solution to update data in real-time.
In this post I want to present you solution how our implementation looked like. In our application we are using ES6 (using Babel) and alt.js as a library delivering Flux features.
We can assume that this is the application for managing blog, so we have only two resources: posts and comments.
The Store
Firstly we will define a store, which will keep information which data we’ve already loaded.
class InitialStateStore {
constructor() {
this.bindListeners({
postsLoaded: PostsActions.allMyPostsFetchedSuccessfully,
commentsLoaded: CommentsActions.allMyCommentsFetchedSuccessfully,
})
this.on('init', () => {
this.reset()
})
}
postsLoaded(response) {
}
commentsLoaded(response) {
}
reset() {
this.loadedData = Immutable.Map({
posts: false,
comments: false,
})
}
}
export default alt.createStore(InitialStateStore);
Code above is a sketch of our alt.js Store. Here we only say, that when an action PostsActions.allMyPostsFetchedSuccessfully
will be triggered, our Store has to call postsLoaded
method. This action will be triggered already when response with posts’ data arrive from the server.
Moreover a reset
method is defined. It’s called in init
callback, so just after our Store will initialize. We want only to set default state here. loadedData
Map will keep information which resources are already loaded. Our flow will look like this:
- We send somewhere (we’ll cover it later) two requests to our backend, one of them fetches all posts, second one fetches all comments.
- When response from server come, it calls
PostsActions.allMyPostsFetchedSuccessfully
action. Similar one for comments. - We “mark” that we’ve already loaded this data in our Store by setting
posts
key ofloadedData
Map totrue
. (Similarly with comments) - If all values in
loadedData
aretrue
it means we’ve loaded all data and we can do something with it - for example turn off loading animation turned on before.
Here’s the code where we set corresponding keys after receiving responses from the server:
class InitialStateStore {
//...
postsLoaded(response) {
this.setState({
loadedData: this.loadedData.set('posts', true),
})
this.checkIfDataLoaded()
}
commentsLoaded(response) {
this.setState({
loadedData: this.loadedData.set('comments', true),
})
this.checkIfDataLoaded()
}
//...
}
Now the last one, the checkIfDataLoaded()
function. As I’ve said before when all data are loaded we want to trigger something - in this case it will be an action which will in result hide loading animation. Note our action is called finishedLoading
- we’ll define it in a while.
class InitialStateStore {
// ...
checkIfDataLoaded() {
let loadedDataAllTrue = this.loadedData.valueSeq().every((v) => { return v; })
if (loadedDataAllTrue) {
setTimeout(() => {
InitialStateActions.finishedLoading()
})
}
}
//...
}
The Actions
Here we’ve implementation of our actions.
startLoading
is an action called somewhere in code. It means “start loading all this initial data”. I’ll not cover implementation ofApi
as it’s fairy simple class returning Promises.finishedLoading
is an action called after all data is loaded.
class InitialStateActions {
startLoading() {
Api.fetchAllPosts().then((response) => {
PostsActions.allMyPostsFetchedSuccessfully(response)
})
Api.fetchAllComments().then((response) => {
CommentsActions.allMyCommentsFetchedSuccessfully(response)
})
this.dispatch()
}
finishedLoading() {
this.dispatch()
}
}
export default alt.createActions(InitialStateActions);
As you can see declaring actions in alt.js is pretty easy, it’s just ES6 class. Actions are methods but only methods containing this.dispatch()
will be dispatched.
Missing details
Now you can ask where to call InitialStateActions.startLoading()
action?
I’ll not cover it with code here, as it belongs more to the topic about authentication. In our application, there are two cases which trigger this action:
- After we receive response from server, that our login is successful - so we have authentication token and may safely download data from server
- When we load login data (user id and authentication token) from cookies - because we’ve authenticated before
Other missing piece is a loading spinner - thanks to Flux we can also pretty much decouple it from the initial state loader. In our case, we’ve a LoaderStore
which solely purpose is managing loading spinner. It’s so short and simple I can even include it whole below:
class LoaderStore {
constructor() {
this.bindListeners({
show: InitialStateActions.startLoading,
hide: InitialStateActions.finishedLoading,
})
this.on('init', () => {
this.loading = false;
})
}
show() { this.setState({loading: true}); }
hide() { this.setState({loading: false}); }
}
Summary
Now, it’s pretty much it. As I said above, only things you need to customize is when you call the InitialStateActions.startLoading()
action in your application and how you display the loading spinner.
As you can see, this solution is pretty generic. It doesn’t interfere with other stores, which makes our application easier to reason about. It follows flux-way of doing things, introducing InitialStateStore
allowed us to remove all fetchFoo
methods scattered around our React components. This in the end lead to the simpler design of the overall app.