After hitting new issues with Iron Router and Blaze 3.0 (see latest entries in Iron Router work with meteor 3, I spend some time investigating what a good way forward could be. Fixing multi-region support would require serious rework of Iron Router middleware was the conclusion, hence with the help of Claude the below proposal has been crafted, which might enrich FlowRouter with simpler subscription management and make life easier for Iron Router legacy users to migrate.
All comments welcome ! Extra ideas or things I overlooked could be added before attempting to implement this.
IronFlow: FlowRouter + Iron Controller Hybrid System
Overview
IronFlow is a proposed hybrid routing solution that combines FlowRouter’s modern async-friendly architecture with Iron Router’s elegant subscription management patterns. This approach addresses the incompatibilities between Iron Router and Blaze 3.0+ while preserving the developer experience that makes Iron Router powerful.
Background
The Problem
- Iron Router + Blaze 3.0+: Iron Router’s synchronous middleware stack conflicts with Blaze 3.0’s async architecture (Fibers removal)
- FlowRouter limitations: FlowRouter lacks Iron Router’s controller pattern and centralized subscription management
- Migration complexity: Moving from Iron Router to FlowRouter requires rewriting subscription handling across the entire application
The Solution
Extract Iron Router’s subscription management components and integrate them with FlowRouter’s routing system, creating a hybrid that offers:
- Iron Router’s familiar controller patterns (
waitOn
,subscriptions
,ready()
) - FlowRouter’s async-compatible architecture
- Progressive migration path from existing Iron Router applications
Architecture Analysis
Iron Router Components (Extractable)
1. WaitList System (ironcontroller/lib/wait_list.js
)
// Reactive readiness tracking
WaitList.prototype.wait = function (fn) {
// Adds functions or subscription handles to wait list
};
WaitList.prototype.ready = function () {
// Returns true when all items in wait list are ready
return this._readyDep.get();
};
Key Features:
- Reactive dependency tracking
- Handles subscription arrays and boolean functions
- Independent of routing system
- Supports subscription handles with
.ready()
method
2. Controller Base Class (ironcontroller/lib/controller_client.js
)
Controller.prototype.wait = function (fn) {
// Delegates to internal WaitList
this._waitlist.wait(fn);
};
Controller.prototype.ready = function () {
// Reactive readiness state
return this._waitlist.ready();
};
Controller.prototype.subscribe = function (/* same as Meteor.subscribe */) {
// Enhanced Meteor.subscribe with .wait() method
var handle = Meteor.subscribe.apply(this, arguments);
return _.extend(handle, {
wait: function () { self.wait(this); }
});
};
Key Features:
- ReactiveDict for component state
- Layout rendering capabilities
- Subscription handle enhancement
- Lifecycle management
3. Hook Collection System (route_controller.js:108
)
RouteController.prototype._collectHooks = function (/* hook1, alias1, ... */) {
// Inheritance-aware hook gathering
// Priority: router global -> route options -> controller prototype -> controller instance
return routerHooks.concat(protoHooks).concat(thisHooks).concat(routeHooks);
};
Key Features:
- Supports multiple hook types (
waitOn
,subscriptions
,onBeforeAction
, etc.) - Inheritance chain traversal
- Router global hooks
- Route-specific hooks
FlowRouter Strengths
1. Async-Compatible Architecture
- No Fiber dependencies
- Promise-based routing
- Compatible with Blaze 3.0+ async patterns
2. Flexible Rendering
- Works with any template system
- BlazeLayout integration
- Server-side rendering support
3. Trigger System
FlowRouter.triggers.enter([function(context, redirect) {
// Route entry hooks
}]);
FlowRouter.triggers.exit([function(context, redirect) {
// Route exit hooks
}]);
Proposed IronFlow Implementation
1. FlowController Class
// packages/ironflow/lib/flow_controller.js
FlowController = function(options) {
// Inherit Iron Controller's subscription management
Controller.call(this, options);
// FlowRouter-specific reactive state
this.params = new ReactiveVar({});
this.queryParams = new ReactiveVar({});
this.routeName = new ReactiveVar('');
// Hook execution tracking
this._hooksExecuted = false;
this._currentContext = null;
};
// Inherit Iron Controller's proven subscription management
FlowController.prototype = Object.create(Controller.prototype);
FlowController.prototype.constructor = FlowController;
// FlowRouter-specific parameter management
FlowController.prototype.setParams = function(params) {
this.params.set(params);
return this;
};
FlowController.prototype.getParams = function() {
this.params.depend && this.params.depend();
return this.params.get();
};
FlowController.prototype.setQueryParams = function(queryParams) {
this.queryParams.set(queryParams);
return this;
};
FlowController.prototype.getQueryParams = function() {
this.queryParams.depend && this.queryParams.depend();
return this.queryParams.get();
};
// Context management for hook execution
FlowController.prototype.setContext = function(context) {
this._currentContext = context;
this.setParams(context.params);
this.setQueryParams(context.queryParams);
this.routeName.set(context.route.name);
};
// Execute Iron Router style hooks
FlowController.prototype.executeHooks = function() {
if (this._hooksExecuted) return;
var self = this;
// Execute subscriptions hooks
var subsList = this._collectHooks('subscriptions');
_.each(subsList, function (subFunc) {
self.wait(subFunc.call(self));
});
// Execute waitOn hooks
var waitOnList = this._collectHooks('waitOn');
_.each(waitOnList, function (waitOn) {
self.wait(waitOn.call(self));
});
this._hooksExecuted = true;
};
// Reset for route change
FlowController.prototype.reset = function() {
this.stop(); // Stop existing subscriptions
this._waitlist = new WaitList;
this._hooksExecuted = false;
this._currentContext = null;
};
2. FlowRouter Integration
// packages/ironflow/lib/flow_router_integration.js
// Global controller registry
IronFlow = {
controllers: {},
// Register a controller for a route
controller: function(routeName, controllerOptions) {
this.controllers[routeName] = new FlowController(controllerOptions);
return this.controllers[routeName];
},
// Get controller for current route
current: function() {
var routeName = FlowRouter.getRouteName();
return this.controllers[routeName];
}
};
// Global route entry trigger
FlowRouter.triggers.enter([function(context, redirect) {
var controller = IronFlow.controllers[context.route.name];
if (controller) {
// Set up controller context
controller.setContext(context);
// Execute subscription hooks
controller.executeHooks();
// Store current controller globally
Session.set('currentController', context.route.name);
}
}]);
// Global route exit trigger
FlowRouter.triggers.exit([function(context) {
var controller = IronFlow.controllers[context.route.name];
if (controller) {
// Clean up controller
controller.reset();
}
Session.set('currentController', null);
}]);
// Template helper for accessing controller
Template.registerHelper('controller', function() {
var controllerName = Session.get('currentController');
return controllerName ? IronFlow.controllers[controllerName] : null;
});
// Template helper for readiness
Template.registerHelper('subsReady', function() {
var controller = IronFlow.current();
return controller ? controller.ready() : true;
});
3. Enhanced BlazeLayout Integration
// packages/ironflow/lib/blaze_layout_integration.js
// Enhanced BlazeLayout with loading awareness
IronFlow.render = function(layoutTemplate, regions) {
var controller = IronFlow.current();
if (controller && !controller.ready()) {
// Render loading template
var loadingTemplate = controller.lookupOption('loadingTemplate') || 'DefaultLoading';
BlazeLayout.render(loadingTemplate);
// Set up reactive rendering when ready
Tracker.autorun(function(comp) {
if (controller.ready()) {
comp.stop();
BlazeLayout.render(layoutTemplate, regions);
}
});
} else {
// Render immediately
BlazeLayout.render(layoutTemplate, regions);
}
};
// Default loading template
Template.DefaultLoading = new Template('DefaultLoading', function() {
return '<div class="loading">Loading...</div>';
});
4. Route Definition Syntax
// Enhanced route definition with controller pattern
FlowRouter.route('/presence/:groupId', {
name: 'presence',
action: function(params, queryParams) {
IronFlow.render('MainLayout', {
main: 'PresencePage'
});
}
});
// Register controller separately for cleaner separation
IronFlow.controller('presence', {
// Iron Router style subscription management
waitOn: function() {
return [
Meteor.subscribe('presenceData', this.getParams().groupId),
Meteor.subscribe('members', this.getParams().groupId)
];
},
subscriptions: function() {
// Non-blocking subscriptions
return Meteor.subscribe('realtime', this.getParams().groupId);
},
data: function() {
return {
members: Members.find({groupId: this.getParams().groupId}),
groupId: this.getParams().groupId
};
},
// Loading template override
loadingTemplate: 'PresenceLoading',
// Hooks
onBeforeAction: function() {
if (!Meteor.userId()) {
FlowRouter.go('login');
return false;
}
},
onAfterAction: function() {
document.title = 'Presence - ' + this.getParams().groupId;
}
});
5. Template Integration
<!-- Using controller data in templates -->
<template name="PresencePage">
{{#if subsReady}}
{{#with controller.getData}}
<h1>Group: {{groupId}}</h1>
{{#each members}}
<div class="member">{{name}}</div>
{{/each}}
{{/with}}
{{else}}
<div class="loading">Loading presence data...</div>
{{/if}}
</template>
<!-- Access controller directly -->
<template name="PresenceStats">
{{#with controller}}
<p>Ready: {{ready}}</p>
<p>Params: {{getParams.groupId}}</p>
<p>Query: {{getQueryParams.filter}}</p>
{{/with}}
</template>
Migration Strategy
Phase 1: Parallel Implementation
// Install FlowRouter alongside Iron Router
meteor add kadira:flow-router
meteor add kadira:blaze-layout
// Create IronFlow package
// Keep existing Iron Router routes working
Phase 2: Route-by-Route Migration
// Convert simple routes first
// Iron Router
Router.route('/simple', function() {
this.render('SimplePage');
});
// IronFlow equivalent
FlowRouter.route('/simple', {
name: 'simple',
action: function() {
BlazeLayout.render('MainLayout', {main: 'SimplePage'});
}
});
Phase 3: Complex Routes with Subscriptions
// Iron Router
Router.route('/complex/:id', {
waitOn: function() {
return Meteor.subscribe('items', this.params.id);
},
action: function() {
this.render('ComplexPage');
}
});
// IronFlow equivalent
FlowRouter.route('/complex/:id', {
name: 'complex',
action: function() {
IronFlow.render('MainLayout', {main: 'ComplexPage'});
}
});
IronFlow.controller('complex', {
waitOn: function() {
return Meteor.subscribe('items', this.getParams().id);
}
});
Phase 4: Remove Iron Router
// Once all routes migrated
meteor remove iron:router
Advantages
1. Familiar Developer Experience
- Same patterns:
waitOn
,subscriptions
,ready()
,data()
- Same syntax: Hook functions with
this
context - Same lifecycle: Controller-based subscription management
2. Technical Benefits
- Async compatibility: No Fiber dependencies
- Blaze 3.0+ support: Works with modern Blaze versions
- Performance: FlowRouter’s lighter routing engine
- Flexibility: Works with any template system
3. Migration Benefits
- Progressive: Migrate routes incrementally
- Risk reduction: Keep critical routes on Iron Router during transition
- Parallel operation: Both systems can coexist
- Code reuse: Existing controller logic largely unchanged
Disadvantages and Limitations
1. Development Complexity
- Two routing systems: Temporary complexity during migration
- Learning curve: Developers need to understand both systems
- Debugging: Two different execution paths
2. Feature Gaps
- Multi-region layouts: Complex region management may need custom implementation
- Server-side routing: Iron Router’s server routing features not included
- Layout inheritance: May need custom implementation
3. Maintenance Overhead
- Custom package: IronFlow would need ongoing maintenance
- Meteor compatibility: Need to track both FlowRouter and Blaze changes
- Community support: Limited compared to established solutions
Implementation Considerations
1. Package Structure
packages/
ironflow/
package.js
lib/
flow_controller.js # Core controller class
flow_router_integration.js # FlowRouter hooks
blaze_layout_integration.js # Enhanced rendering
wait_list.js # Extracted from iron:controller
reactive_dict.js # State management
templates/
loading.html # Default loading template
tests/
controller_tests.js
integration_tests.js
2. Dependencies
// package.js
Package.describe({
name: 'ironflow',
version: '1.0.0',
summary: 'FlowRouter + Iron Controller hybrid system'
});
Package.onUse(function(api) {
api.versionsFrom('2.8');
// Core dependencies
api.use(['ecmascript', 'tracker', 'reactive-var', 'reactive-dict']);
// FlowRouter integration
api.use(['kadira:flow-router', 'kadira:blaze-layout']);
// Iron Controller components (extracted)
api.use(['iron:controller', 'iron:layout']);
// Template system
api.use(['templating', 'blaze-html-templates']);
});
3. Testing Strategy
// Test controller lifecycle
Tinytest.add('IronFlow - Controller lifecycle', function(test) {
var controller = new FlowController({
waitOn: function() {
return Meteor.subscribe('test');
}
});
// Test hook execution
controller.executeHooks();
test.isFalse(controller.ready());
// Simulate subscription ready
// Test ready state change
});
// Test FlowRouter integration
Tinytest.add('IronFlow - Route integration', function(test) {
FlowRouter.route('/test', {name: 'test'});
IronFlow.controller('test', {
waitOn: function() {
return Meteor.subscribe('test');
}
});
// Test route navigation triggers controller
FlowRouter.go('/test');
test.isTrue(IronFlow.current() instanceof FlowController);
});
Performance Characteristics
1. Memory Usage
- Controller instances: One per route (similar to Iron Router)
- Reactive dependencies: WaitList, ReactiveVar, ReactiveDict per controller
- Cleanup: Automatic on route exit
2. Rendering Performance
- Loading states: Immediate loading template rendering
- Reactive updates: Efficient subscription readiness tracking
- Template caching: BlazeLayout handles template reuse
3. Network Efficiency
- Subscription management: Same as Iron Router (proven efficient)
- Route changes: FlowRouter’s optimized change detection
- Data reactivity: Maintains Meteor’s reactive data system
Conclusion
IronFlow represents a practical solution to the Iron Router + Blaze 3.0 compatibility crisis. By extracting Iron Router’s proven subscription management patterns and integrating them with FlowRouter’s modern architecture, it provides:
- Immediate solution: Resolves Blaze 3.0 compatibility issues
- Familiar patterns: Preserves developer experience and existing knowledge
- Migration path: Allows gradual transition from Iron Router
- Future-proof: Built on FlowRouter’s sustainable foundation
The hybrid approach acknowledges that Iron Router’s controller pattern and subscription management are genuinely valuable innovations that shouldn’t be discarded, while embracing FlowRouter’s technical advantages for long-term sustainability.
Recommendation: For applications heavily invested in Iron Router patterns, IronFlow offers the most practical migration path that preserves both functionality and developer productivity while ensuring compatibility with modern Meteor and Blaze versions.