Recently at work, I was pulled into a project where React is used to generate a nested form and aggregate them at the end for a user. React is pretty good at handling the nested form display based on conditions selected by the user on the fly. The problem comes when the form needs to be sent via Ajax to a controller. Because the form is nested, we have to keep track and update state at each level. It's not only tedious but hard to maintain in the long term.
Now Flux comes to rescue. Flux is a React architecture. Unlike MVC, data flow in Flux is unidirectional.
From the graph shown above (in Flux official release page), an Action triggers a Dispatcher. A dispatcher is a global action handler that broadcasts payloads to registered callbacks. A store manages the data state, data retrieval methods and dispatcher callbacks. So in our nested form case, each level of the form sends action to the application store directly instead of maintaining their own individual state.
Let's look at a simple example of how Flux works.
In this example, we have an text input area and an "Add" button. Once user press the button, input text shows up on the page in the order.
var AddMessageForm = React.createClass({
render: function() {
return (
<div>
<label>Message</label>
<textarea type="text" ref="textarea" rows="8" cols="30"/>
<button type="submit" >Add</button>
</div>
)
}
})
It shows as:
Now we register an action when the button is clicked.
<button type="submit" onClick={this.saveToList} >Add</button>
What happens on click? Your view dispatches a well defined event, with the event name and data.
First of all, we initiate a dispatcher
.
var AppDispatcher = new Flux.Dispatcher();
The method triggered by click the button from view.
saveToList: function() {
AppDispatcher.dispatch({
eventName: 'add-message',
newText: data
})
}
Now we need a store
to respond to dispatched events. We name it AppStore
, which is a global object.
var AppStore = {
_listeners: [],
data: {
messages: messages
},
emitChange: function(){
this._listeners.forEach(function(listener) {
listener(this);
}, this);
},
addChangeListener: function(callback) {
this._listeners.push(callback);
},
removeChangeListener: function(callback) {
var idx = this._listeners.indexOf(callback);
this._listeners.splice(idx, 1);
}
};
It stores our data. emitChange
allows AppStore
to listen/broadcast events.
Register dispatcher callback.
AppDispatcher.register(function(payload) {
switch( payload.eventName) {
case 'add-message':
AppStore.data.messages.push(
{
"text": payload.newText
});
AppStore.emitChange();
break;
}
})
We register a callback with our AppDispatcher
using its register method. Store is then listening to AppDispatcher
broadcasts. Switch statement determines any matched actions to take. Each payload contains an event name and data. If a relevant action is taken, a change event is emitted, and views that are listening for this event will update their states.
In this case, if store detects an action named add-message
, it'll push the new message text sent via payload
to its message array. The message array in store gets updated with current data. Now we'll see how the view will be updated as well.
First, we need to listen for the change event from our AppStore.
componentDidMount: function() {
AppStore.addChangeListener(this.dataChanged)
},
dataChanged: function(){
this.setState(AppStore.data);
}
We also need to remove the event listener when it's unmounted.
componentWillUnmount: function() {
AppStore.removeChangeListener(this.dataChanged);
}
Last but not least let's look at our view render function, which will listen to the state change and update itself accordingly.
render: function() {
var messages = AppStore.data.messages;
var messageList = messages.map( function( message ) {
return (
<li >
{ message.text }
</li>
);
});
return (
<div>
<AddMessageForm />
<ul>
{ messageList }
</ul>
</div>
);
}
Now we complete our entire chain of action -> dispatcher -> store -> view
. In this case you add a new message, the view/button click dispatches an action. Then dispatcher registered callback responds to the action and updates store state. The store triggers a change event and view is updated with latest state.
If I input something in the text box:
and click "Add" button. The message will show up at the bottom as expected.
I like Flux architecture because it makes view, action, update all clear and straightforward. Store, dispatcher and view each has its well defined responsibility and do not intervene each other.