A tutorial how to create a universal ReactJS application with Flux.
Full code of the application is accessible here.
The important thing to notice is that we hold the state of the app in many places. In a more complicated application it can cause a lot of pain :)
In this post we will update our app to use a more structured pattern for managing the state – Flux.
Using bare ReactJS was easy, but our application is simple. With lots of components, having the state distributed all over them would be really tricky to handle.
Facebook experienced such problems, from which a very well known one was the notification bug.
The bug was that you saw the notification icon indicating that you have unread messages, but when you clicked the button to read them, it turned out that there’s actually nothing new.
This bug was very frustrating both for users and for Facebook developers, as it came back every time developers thought they already fixed it.
Finally, they realized that it’s because it’s really hard to track updates of the application state. They have models holding the state and passing it to the views, where all the interactions happen. Because of this, it could happen that triggering a change in one model caused a change in the other model and it was hard to track how far to other models these dependencies go.
Summing up, this kind of data flow is really hard to debug and maintain, so they decided they need to change the architecture completely.
So they designed Flux.
First of all, you need to have in mind that Flux is an architecture, an idea. There are many implementations of this idea (including the Facebook one), but remember that it’s all about the concept behind them.
And the concept is to have all the data being modified in stores.
Every interaction that causes change in the application state needs to follow this pattern:
You can have many stores and there is a mechanism to synchronise modifications done by them if you need it.
I recommend that you read a cartoon guide to Flux, the architecture is explained really well there, and the pictures are so cute! :)
A thing worth emphasising is that some components will require their own state. We will call them “smart components”. Others, responsible only for displaying the data and attaching hooks, we could call “dumb components”.
“Smart components” don’t modify their state by themselves – like I mentioned earlier, every state change is done by dispatching an action. They just update their state by using a store’s public getter.
“Dumb components” get the state by passing needed items through props.
Let’s add new dependencies to our package.json by running: npm install –save flux events.
As I said, all state changes need to be done by dispatching actions. We need to create src/AppDispatcher.js then:
import { Dispatcher } from 'flux'; | |
class AppDispatcher extends Dispatcher { | |
handleViewAction(action) { | |
this.dispatch({ | |
source: 'VIEW_ACTION', | |
action: action | |
}) | |
} | |
} | |
const appDispatcher = new AppDispatcher(); | |
export default appDispatcher; |
It’s good to have all action types defined in one file. Create a src/constants directory with ActionTypes.js inside:
export default { | |
REQUEST_SUBMISSION: 'REQUEST_SUBMISSION', | |
PERFORM_RATING: 'PERFORM_RATING' | |
}; |
Now we will define the SubmissionActionsCreator:
import ActionTypes from '../constants/ActionTypes'; | |
import AppDispatcher from '../AppDispatcher'; | |
const SubmissionActionsCreator = { | |
requestSubmission(id) { | |
AppDispatcher.handleViewAction({ | |
actionType: ActionTypes.REQUEST_SUBMISSION, | |
id: id | |
}) | |
}, | |
performRating(id, rate) { | |
AppDispatcher.handleViewAction({ | |
actionType: ActionTypes.PERFORM_RATING, | |
id: id, | |
rate: rate | |
}) | |
} | |
} | |
export default SubmissionActionsCreator; |
SubmissionActionsCreator uses AppDispatcher to dispatch needed actions.
As you can see, an action is just a simple Javascript object with data that the store will need to calculate the state change.
An important key that will be always present in action object is actionType – one of the constants listed in the ActionTypes.js file.
Here we also need the submission id and sometimes rate.
Now we can update our smart SubmissionPage component to use SubmissionActionsCreator instead of just directly accessing the API:
// ... | |
import SubmissionActionsCreator from '../actions/SubmissionActionsCreator'; | |
class SubmissionPage extends React.Component { | |
// ... | |
performRating(value) { | |
const id = this.state.submission.id; | |
SubmissionActionsCreator.performRating(id, value); | |
} | |
// ... | |
}; | |
export default SubmissionPage; |
And the last thing we need is to add the store where our state will live:
import { EventEmitter } from 'events'; | |
import {ReduceStore} from 'flux/utils'; | |
import AppDispatcher from '../AppDispatcher'; | |
import ActionTypes from '../constants/ActionTypes'; | |
import Connection from '../lib/Connection'; | |
const CHANGE_EVENT = 'change'; | |
class SubmissionStore extends EventEmitter { | |
constructor() { | |
super(); | |
this.submission = null; | |
} | |
getSubmission(submissionId) { | |
return this.submission; | |
} | |
addChangeListener(listener) { | |
this.addListener(CHANGE_EVENT, listener); | |
} | |
removeChangeListener(listener) { | |
this.removeListener(CHANGE_EVENT, listener); | |
} | |
emitChange() { | |
this.emit(CHANGE_EVENT); | |
} | |
} | |
const submissionStore = new SubmissionStore(); | |
submissionStore.dispatchToken = AppDispatcher.register((payload) => { | |
let id; | |
switch (payload.action.actionType) { | |
case ActionTypes.REQUEST_SUBMISSION: | |
id = payload.action.id; | |
Connection.get(`/submissions/${id}`).then((response) => { | |
submissionStore.submission = response.data; | |
submissionStore.emitChange(); | |
}); | |
break; | |
case ActionTypes.PERFORM_RATING: | |
id = payload.action.id; | |
const rate = payload.action.rate; | |
Connection.post(`/submissions/${id}/rate`, { rate: rate }).then( | |
(response) => { | |
submissionStore.submission = response.data; | |
submissionStore.emitChange(); | |
}); | |
break; | |
default: | |
// Do nothing | |
} | |
}); | |
export default submissionStore; |
Notice also the AppDispatcher.register part, where we do the actual request to the API, update the store state on success and notify all subscribed components that the state has changed.
Now we can update our smart SubmissionPage component to use SubmissionStore.
The whole SubmissionPage class should look like this:
import React from 'react'; | |
import Rate from './Rate'; | |
import SubmissionStore from '../stores/SubmissionStore'; | |
import SubmissionActionsCreator from '../actions/SubmissionActionsCreator'; | |
class SubmissionPage extends React.Component { | |
constructor(props) { | |
super(props); | |
this.state = { submission: {} }; | |
this._onChange = this.onChange.bind(this); | |
} | |
componentDidMount() { | |
const id = this.props.params.id; | |
SubmissionActionsCreator.requestSubmission(id); | |
} | |
componentWillMount() { | |
SubmissionStore.addChangeListener(this._onChange); | |
} | |
componentWillUnmount() { | |
SubmissionStore.removeChangeListener(this._onChange); | |
} | |
onChange() { | |
this.setState({ submission: SubmissionStore.getSubmission() }); | |
} | |
performRating(value) { | |
const id = this.state.submission.id; | |
SubmissionActionsCreator.performRating(id, value); | |
} | |
render() { | |
const submission = this.state.submission; | |
return ( | |
<div> | |
<div className="submission"> | |
<h2>Submission</h2> | |
<ul> | |
<li>Id: {submission.id}</li> | |
<li>First Name: {submission.first_name}</li> | |
<li>Last Name: {submission.last_name}</li> | |
</ul> | |
</div> | |
<Rate rate={submission.rate} performRating={this.performRating.bind(this)} /> | |
</div> | |
) | |
} | |
}; | |
export default SubmissionPage; |
In componentDidMount we use SubmissionActionsCreator to dispatch requestSubmission.
Because in componentWillMount we subscribe for store change using addChangeListener, we will be notified when the submission is loaded from the API.
Remember to unsubscribe in componentWillUnmount.
Thanks to the subscription, the onChange method will be called on store state change. And in onChange method we can update the local state to the current store state then.
Exactly the same mechism is used in performRating.
We updated our application to use the Flux architecture. It’s definitely an improvement over using bare ReactJS. We have more control over the application state.
But it has some downsides too. If the application grows and there are a lot of stores it’s hard to synchronize changes, especially when the stores depend on each other.
I will write more about this in the next post, where we’ll introduce Redux to our application.
For now, you can practise a bit by fluxifying the rest of the application.
Full code accessible here.
See you next week!
We don’t just build software, we understand your business context, challenges, and users needs so everything we create, benefits your business.