Recently in JavaScript, and especially in ClojureScript world, one approach of developing front-end apps became pretty popular – using unidirectional app flow and persistent data structures. Something pretty close to Flux, but not really exactly like it. I tried that approach in Textik, and was really happy how it went.
The idea
Here’s the idea – in your app, you have only one place where you store ALL of your application state, some sort of global structure, containing persistent vectors and maps. It is important to use persistent data structures there, as you will see soon, they provide some very important features we use to speed up our app.
So, you have one global data structure, where the state of all your application is stored. Everything is there – what tooltips are shown, what buttons are disabled, all the content of the app, etc. Then, you pass that global data structure to your rendering engine, which renders the whole app, from top to bottom, for the first time. It also sets up various HTML events listeners to the DOM elements, and attaches event handlers, which basically do only one thing – they generate parameters payload, and pass it to Dispatcher.
Dispatcher receives that payload, and figures out what to do with it. It calls various “mutators” – models, which actually will change the global state of the app. Then, Dispatcher calls the rendering engine again, and passes the updated global state to it. The rendering engine rerenders the whole app again, from top to the bottom. But now, it can easily figure out what parts, or what components of the app should be rerendered. We pass particular subtrees of our global state tree to the components while rendering the whole app, and because these are persistent data structures, we can very quickly find out if some particular subtree was changed since last render. So, the rendering engine, using that knowledge, rerenders only parts of the app, which were changed, and because of that – it does that really fast.
And then everything happens in the same way again. We get another event, send the payload to Dispatcher, Dispatcher calls mutators, mutators change the global state, and Dispatcher calls the rendering engine to rerender the app again.
I.e., this way:
1 2 3 4 5 |
|
So, this way we get a very simple data flow, all our events go through the same message bus to Dispatcher, the app structure becomes so simple, it is even ridiculous a bit :)
Implementing TodoMVC in Dart
So, I decided to try the same approach with Dart. I took ReactJS as the rendering engine, since it works really close to what I want from it, and there is a nice Dart wrapper for it from Clean Team – react. As for persistent data structures, there is a pub package from VacuumLabs – persistent (but unfortunately it is not in Pub, you should get it from their forked repo from GitHub).
You can check the source code here: https://github.com/astashov/todomvc/tree/master/labs/architecture-examples/react-dart-uniflow/web/dart, and you can play with it here: http://astashov.s3-us-west-2.amazonaws.com/todomvc/index.html (if you want to check it in Dart, use Dartium). This is just usual TodoMVC app.
Let me quickly guide you through the code.
Everything starts with initializing the global state.
1 2 3 4 5 6 7 8 9 10 |
|
appData
will be globally available, and we’ll get the current value of the global state with appData.value
.
There are a bunch of methods-mutators in the Data
class, which will overwrite the value
property of appData
with the new value. They won’t really mutate the existing value
, but recreate the new one, reusing unchanged parts of the old value
.
Then, we initialize the app, entering our main()
function:
1 2 3 4 5 6 |
|
setClientConfiguration()
initializes the react
library, and then we render the app for the first time. It happens in dispatcher.dart:
1 2 3 4 5 |
|
Here we call renderComponent
, which is a function of the react
package, and pass the React component we are going to render and the selector which we are going to append it to.
The components are defined in the components.dart file.
All our components are not just default React
components, but instead inherited from our custom _Component
class, which we instrumented with the shouldComponentUpdate
hook. There we check if the new passed value (our subtree of the global state) was changed since the last render. Again, this check should be very fast because of the persistent data structures properties. Also, as a convention, we are going to send our subtrees as the ‘value’ prop.
Let’s have a look at todoAppComponent
– our entry component. All it does is just renders three subcomponents – header, footer and main, and pass parts of the received global tree to them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Now, let’s have a look e.g. at headerComponent
, which renders the input for creating new tasks, and also adds event handlers to the input. It only receives the appData.value['new-input']
value, which defines what should be the value of the input.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Every single time we enter/remove a character in the input, we fire an event, and its handler calls the dispatch function of Dispatcher. We send the payload to it, which looks like {'action': 'new-input', 'value': input.value}
.
There, in Dispatcher, we handle it. For simplicity, I mutate the global state right in Dispatcher, but in a larger application you probably would want to delegate that to a model.
And then, we rerender the app again.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Summary
So, that’s actually it. Of course there are also other functionality – editing items, removing items, marking as completed, etc, but it works just in the same way as I just described. For items, there is the Item
model – items.dart, which provides API for CRUD operations for items. But all that stuff works in the same way I just described.
That’s the whole lifecycle of the application. Very simple, very declarative – you basically only need to work with the global state, changing it accordingly, and you don’t really need to care about the view – React will do a great job with rendering the app using the global state.
Another cool side-effect of having the global state – you can always easily reproduce your user’s problems – debugging becomes way easier! If you use something like Airbrake or Rollbar to aggregate exceptions from your users, then you could just attach the global state in JSON to the exception you are about to send to e.g. Rollbar, and then you can easily reproduce the user’s problem just by applying that JSON on your machine, so you’d get exactly the same state of the app where the user was before the exception happened. Kinda cool, huh? :)
There are a bunch of other cool things (e.g. simple undo – you can just save the full state of the app (or part of the state) in a vector every time the change happens, and it will be stored efficiently because of the nature of persistent data structures).
I’m really excited about this approach so far.
Isn’t that like Rails?
If you ever worked with Rails, you can actually see a lot of similar things in this approach. In Rails, we pass our request through router, then router figures out (from URLs and GET params) what controller this request should be sent to, controller calls various models, which change the database, and the render the response. In this case,
- Dispatcher is router
- ‘mutators’ are models
- appData is our database
In models, we also don’t have our internal state, models work with the database, retrieving and saving data into it. Same as in ActiveRecord :)
And the whole lifecycle of the front-end app like this one and a Rails app looks extremely familiar. :)
There are still some things, that could be improved, and some additional persistent data structures, that could be written, like e.g. currently PersistentVector doesn’t efficiently handle inserting/removing elements in the middle of a vector. But still, you already can do a lot with the Dart ecosystem and these 2 packages I was talking about – react
and persistent
.
So, please try it out, hopefully you’ll find it useful :)
Related links
- React
- Persistent – btw, there is a great explanation why persistent data structures are cool, make sure you’ve read that!
- Source code
- Demo (Dartium only)
- Slides of my Textik talk – where I was talking about the Textik’s architecture, which is very close to what I described dhere.