I have been trying to figure out for a while the best way to do extensible apps with Meteor for the next version of Worona. I will share my experiences with Flux and Meteor in case they are of some use here.
Sometimes I read about how Meteor and Flux relate to each other on posts or threads. I don’t think there is a direct resemblance. Meteor is a platform and Flux is an architecture.
I think there is a lot of confusion because people try to use other JS Flux implementations (created without Meteor in mind) in Meteor instead of just applying the principles of the architecture.
In my opinion, Flux just brings these principles to the table:
- Total decoupling of the View layer using the Dispatcher for events and a single object tree State for data (this is from Redux).
- One way data flow (easier debugging).
- No chained actions (easier debugging).
- Easier structure and code organisation.
I think each of those principles are good for a Meteor app because by default, Meteor comes without rules.
The View layer decoupling is very important. Designers should be able to write the UI with no/minimum javascript use.
Dispatcher is important as well. Its event pattern allows to easily extend the app when something happens.
No chained actions is very useful as well because it forces you to think easier ways to solve the same problem.
One way data flow makes reasoning about your app flow much easier.
What I mean by easier structure and code organisation is that when using Flux, you know that views can only dispatch, stores contain logic and modify data, they are structured by domain, state is a single object tree, and so on. So you don’t have to think where to put (or find!) stuff, you just write code. This code organisation is another thing Meteor doesn’t have.
At first, I tried to make Flux work with a port of the Facebook’s dispatcher. It was great, but if found the Flux’ “Action-driven” architecture collides with Meteor’s “Data-driven” mentality. I tried to solve it but:
- When I tried using only Flux actions to change the state of my app, the reactive data sources where left behind.
- When I tried to send a Flux action each time Meteor was changing some data, Meteor reactivity lost its magic and I ended up with a lot of not useful boilerplate code.
So that approach wasn’t working. Besides I wanted to introduce a single object tree for the whole state of the app in a kind of a Redux style.
I am a huge fan of Tracker. Reactive code is way easier to write and maintain. So it didn’t make sense to not use reactivity for the single object tree. I wrote a package called ReactiveState and it is working really well. People is using it even for other non-Flux apps.
Then, I went back and forth with Flux until I figured out a way to make both worlds (Flux’ Action-driven and Meteor’s Data-driven) work together. The solution was to make Flux actions reactive, but still have a non-reactive phase at the beginning and at the end of each dispatch.
The result is the MeteorFlux framework. I consider it still in development but so far it is working really well. GitHub - luisherranz/meteorflux: Flux architecture for Meteor
The Dispatcher starts the one-way-data-flow and it looks like this:
<a href="#" dispatch="CHECK_WP_API">Try again</a>
or in javascript:
Template.ApiChecker.events({
'click .change-url'(event, template) {
let url = template.find('input[name=url]');
Dispatch('APP_CHANGED', { url });
}
});
That’s it. Views aren’t allowed to do anything else.
After the dispatch, you can write two types of callbacks:
- Normal (non-reactive) Store callbacks:
Register(() => {
switch (Action.type()) {
case 'NEW_APP_CREATED':
Meteor.call('addNewApp', data);
break;
case 'APP_CHANGED':
let id = State.get('app.id');
Meteor.call('changeApp', id, data);
break;
}
});
// or async actions as well:
Register(() => {
if (Action.is('LOGOUT')) {
Meteor.logout( function(error) {
if (!error) {
Dispatch('LOGOUT_SUCCEED').then('SHOW_LOGIN');
} else {
Dispatch('LOGOUT_FAILED', { error });
}
});
}
});
- And then use reactive State modifications (called reducers in Redux):
// dependent on Flux actions...
State.modify('apiChecker.error', (state = false) => {
switch (Action.type()) {
case 'API_CHECK_FAILED':
return true;
case 'CHECK_API':
case 'API_CHECK_SUCCEED':
return false;
default:
return state;
}
});
// or dependent on Meteor reactive sources...
State.modify('apps.items', (state = []) => {
return Apps.find({}, { sort: { modifiedAt: -1 } });
});
let handle = Meteor.subscribe('apps');
State.modify('apps.isReady', (state = false) => {
return (!!handle && handle.ready());
});
State.modify('app', (state = {}) => {
let appId = State.get('app.id');
return Apps.findOne(appId);
});
Finally use State to render the View. It is available everywhere, designers don’t need helpers.
<div class="ui grey header">
Welcome {{profile.firstName}}!
</div>
<button class="ui button" dispatch="OPEN_NEW_APP_FORM">
Add another app!
</button>
<div class="ui subheader">
Choose an app:
</div>
{{#if apps.isReady}}
<div class="ui cards">
{{#each apps.items}}
<button class="header" dispatch="SHOW_APP" data-id={{_id}}>
{{name}}
</button>
<div class="meta">
<strong>Url:</strong> {{url}}
</div>
<div class="meta">
<strong>Modified:</strong> {{moFromNow modifiedAt}}
</div>
{{/each}}
</div>
{{else}}
Apps are loading, please wait!
{{/if}}
The log of the app is easy to follow and looks like this:
What the user did here was to enter its email and password on a login form. He didn’t have account yet, so we created a new one for him and showed him a special form called “Create your first app” asking for his name as well. When he submitted the form, the profile was changed, a new app was created and he is sitting now in the screen which shows him his list of apps.
This combination of Tracker and Flux is working really well so far. Logic is easy to write, strong against bugs (thanks to Tracker) easy to reason about and easy to extend (thanks to Flux).
The view layer is also easier to write and any designer can jump in. They only need a list of actions they can dispatch and the available tree of State. They shouldn’t care if data is coming from an external API, Minimongo, a javascript variable… whatever.
I don’t have easier examples to show at the moment other than the product we are building:
It is still in the first steps but if you are interested, you can get an idea. We are using “everything-is-a-package” as well. You can take a look at the action.js and state.js files of the dashboard packages to see more code examples.