Introduction
In these days I’m digging deep in the wonderful React.js library, widely used by Facebook and Instagram for their UIs. This library uses an abstraction of the DOM, called Virtual DOM, to render components in the View. Honestly I’m was not a big fan of DOM abstractions, because in my opinon it affects both the work of developers and designers. HTML should not be mixed with JavaScript and viceversa. I’ve worked in the past years with declarative libraries like Knockout.js, so at first I was disoriented by this new approach. However after spending a couple of days on the docs I was really blown away by the power and the intuitiveness of the library. By the way, take a look at this blog post by Pete Hunt which presents a clear approach to start thinking in React.
React.js “makes no assumptions about the rest of the technology stack”, therefore it can be used alone or in combination with other libraries. However it’s always used along with another Facebook library called Flux, which enforces unidirectional data flows. In this sense, Flux is more a design pattern, opposed to the popular MVC, which defines clear dependencies between the different parts of an application (Action creator, Dispatcher, Store and Views). In this way it’s simpler to track changes and bugs during the development process. Unfortunately its documentation is a bit poor, so I decided to stick with Fluxxor, one of the many complementary tools for React. Fun note: a couple of libraries implementing and reifying Flux concepts, including Fluxxor, have names inspired by the popular movie trilogy Back to the Future. FTW!
Fluxxor facilitates the process of building the data layer of the application by reifying many of the core Flux concepts. Moreover its documentation is clear and concise, and the examples are complete enough to make you start.
Luckily with this architecture it’s still possible to use (still with some difficulties) plain old JavaScript/jQuery plugins, so I decided to present a way to integrate the popular Dropzone.js library into an application made with Fluxxor and React.js.
We begin with the UI. Let’s say we are building a Facebook album clone, which presents, in vertical order: the title of our album, a list of images and a footer with the Uploader component. We have to call it with a different name because the Dropzone library extends the JavaScript window with a Dropzone object, thus we don’t want to overwrite it. The component hierarchy is the following:
- Album
- AlbumTitle
- ImageList
- Uploader
Keep in mind that “a component should ideally only do one thing. If it ends up growing it should be decomposed into smaller subcomponents.”. Let’s code!
var Album = React.createClass({
// mixins used by this class
mixins: [FluxMixin, StoreWatchMixin("AlbumStore")],
getStateFromFlux: function() {
var store = this.getFlux().store("AlbumStore");
return {
album: store.album
};
},
// action to perform when upload completes
handleUpload : function(image) {
this.getFlux().actions.addImageSuccess(image);
},
// render the component
render: function() {
return (
<div className="album">
<header>
<AlbumName name={this.state.album.name} />
</header>
<section className="content">
<ImageList images={this.state.album.images} />
</section>
<footer>
<Uploader onUploadComplete={this.handleUpload} />
</footer>
</div>
);
}
});
In this React class we are using the FluxMixin which allows us to interact directly with the actions. This is particularly useful when we have to update the data model from a deeply nested component. In fact in React “the suggested way to communicate between components is to simply set props when communicating from parent to child and to pass callbacks through props when communicating from child to parent”. The Album
class also depends on the AlbumStore
. In Flux, stores are responsible for managing business logic and data and are updated only using through the Dispatcher. Stores respond with specific handlers to specific synchronous actions. However they can also perform asynchronous operations, like AJAX calls.
Using the getStateFromFlux()
function we can get the JSON representation of our album, fetched by the store. The name property is passed to the AlbumName
component, while the array of images is passed to the ImageList
component. Finally the Uploader
component receives the handleUpload
callback.
Let’s take a look on how the Uploader
component is built:
var Uploader = React.createClass({
// invoked immediately after mounting occurs, initialize plugins
componentDidMount: function() {
var self = this;
// disable dropzone autodiscover
Dropzone.autoDiscover = false;
// istantiate dropzone
var myDropzone = new Dropzone(this.getDOMNode(), {
url : '/api/upload'
});
myDropzone.on("complete", function(file) {
// callback on Album class
self.props.onUploadComplete(file);
});
},
render : function() {
return (
<form action="/api/upload" className="dropzone" id="dropzone">
<div className="dz-default dz-message text-center">
<i className="fa fa-cloud-upload fa-4x"></i>
</div>
</form>
);
}
});
This is pretty straightforward. The key is the componentDidMount()
function which allows to attach the Dropzone object to the node rendered, using the method getDOMNode()
.
Next we have our actions, the only way to update Stores. These are simply a combination of a string type and an object payload. The function addImage()
takes the JSON server response, from the upload call, and dispatches its content along with the constant UPLOAD_ALBUM_IMAGE_SUCCESS
:
var actions = {
addImageSuccess: function(image) {
this.dispatch("UPLOAD_ALBUM_IMAGE_SUCCESS", image);
}
}
The Flux Store, called AlbumStore
, is responsible for listening to the upload success action. It pushes the new image into the album and emits the following “change” event which updates the view.
var AlbumStore = Fluxxor.createStore({
initialize: function() {
this.album = {};
this.bindActions(
// ...
"UPLOAD_ALBUM_IMAGE_SUCCESS", this.onUploadAlbumImageSuccess
);
},
onUploadAlbumImageSuccess : function(image) {
this.album.images.push(image);
this.emit("change");
}
});