React + Flux is all the rage in the frontend world these days. In Flux, data is kept in “stores” which contain the state necessary to render views to the screen. Stores are designed to make rendering the views easy, and mix UI-level state (such as what field the user has focused) with data from the server (such as the username of the currently logged-in user).
This makes it easy to build contained UI components, but at some point data needs to be exchanged with the server. The server understands resources and actions on those resources (CREATE a post, DELETE a user, etc…), which is not necessarily how data is represented in Flux stores. The Flux spec from Facebook leaves the actual server communication side of things pretty hazy, specifying only that server interaction should be triggered in action creators.
Quick overview of the Flux architecture.
If you’re like us, data on the server is exposed via a REST(ish) API, and it would be ideal for Flux to communicate to the server via that API (communicating with REST APIs was previously all the rage in the frontend world with Backbone and Ember, but that was at least 10 minutes ago). At Quizlet, we took a hybrid approach between Flux stores and traditional RESTful models.
We initially considered using existing model implementations such as Backbone models, but decided against it for several reasons. First, Flux and React work best with immutable objects, which Backbone models aren’t. Second, we wanted our syncing implementation to be smart about retries and batching similar updates from different sources together. And third, we didn’t need a lot of the features of Backbone models, such as emitting events on change.
The system we created has two representations of each RESTful model - one is a ViewModel which is immutable, disposable, and represents the state of that model in the UI, and the other is a ServerModel which is mutable and represents the state of that model on the server. ServerModels and ViewModels have identical sets of properties, but the ViewModel may have additional view-specific helpers as well. This lets our UI deviate from what’s represented on the server without forcing all changes to be persisted immediately, and gives us a clear boundary between the server and our UI. Furthermore, the immutable nature of ViewModels means we can use them efficiently within React views.
We use a Syncer class to manage the interaction between ServerModels and ViewModels. Syncers can take a ViewModel and persist it, thus reconciling it with its ServerModel. Syncers can also go the other direction, generating ViewModels from the data on the server. Syncers also can be used to create new ViewModels with only local IDs that have not yet been saved to the server. Our syncers are smart enough to handle retries on failed requests, and can batch similar updates together.
Syncers control persistence and loading via the API. Interactions with syncers happen inside of action creators.
Our Flux stores contain ViewModels as well as any other UI-level data they need to draw various components to the screen. Our Flux stores typically don’t touch ServerModels directly since stores are ideally tied to the UI and shouldn’t need to worry about what the server is doing.
All data persistence to and from the server is managed in our Flux action creators. Action creators determine when to tell syncers to write out ViewModels to the server, when to load models from the server, and when to create new View Models. When syncing completes, a corresponding action gets triggered containing updated new ViewModels which the stores can listen for to keep themselves up to date as changes come through.
A Quick Example
To see how this system works in practice, let’s walk through an update cycle to a term in a study set on Quizlet. A term consists of a pair of word and definition text that a student may use to study. We recently built a new way to edit your terms that uses React + Flux, illustrated below:
Editing a term in Quizlet
As the user types, we make calls to action creators which update the TermViewModel for this term. The UI is rendered from that TermViewModel, so changes are reflected as the user types. We don’t persist those changes on every keystroke, though, as that would be a lot of API requests.
When the user finishes typing and the input loses focus, we persist the changes to the server. This means passing the most recent TermViewModel to the TermSyncer and calling save. If the request succeeds then the TermSyncer will return the server response and trigger an action for the the stores to update the TermViewModel. If the request fails the TermSyncer will automatically retry, and if it for some reason it can’t save then the user will be presented with an error.
We’ve found this sort of hybrid system works well to reconcile our RESTful API with Flux’s UI-focused view of data. Yahoo recently released their Fetchr project which tackles a similar problem in a different way. We’d love to hear any other solutions to this problem that other React devs have come up with. And of course, if you want to work on more problems like this at scale, we're hiring!