Iron Router works with Meteor 3!

Hi,

I’m experiencing an issue where this.data in my template’s onCreated callback is wrapped in an extra value object, which shouldn’t be the case.

Here’s my router setup:

javascript

Router.route('/myRoute', {
  name: 'myRoute',
  data() {
    return MyCollection.findOne();
  },
});

And in my template:

javascript

Template.myRoute.onCreated(function () {
    console.log(this.data);
    // Currently shows: {value: myData}
    // Expected: myData (the actual document from MyCollection)
});

Expected behavior: this.data should directly contain the document returned by MyCollection.findOne().

Actual behavior: this.data contains {value: myData} where myData is wrapped inside a value property.

Has anyone encountered this issue? What could be causing this extra object wrapper, and how can I get the direct data without the value wrapper?

Thanks

Aletapia

There is no need to use async calls on the client side. Typically, your iron router usage is on the client side, so keep using as you would normally do.

Polygonwood

Dayd

I believe the normal pattern is to pass the data in the rendering call :

Router.route('/post/:_id', function () {
  this.render('Post', {
    data: function () {
      return Posts.findOne({_id: this.params._id});
    }
  });
});

Check the Iron Router manual for more examples.

I’ve always used the second way to declare a route, see the “Route Specific Options” section, but I’ll give the other method a try. Thanks!

https://iron-meteor.github.io/iron-router/#route-specific-options

But I would prefer to use the object version :slightly_smiling_face:

Might you consider doing a short talk on Iron Router on MeteorJS 3.x for Impact 2025? It may be useful to people who still need to upgrade their 1.x-based apps …

2 Likes

I’m just a amateur package maintainer - when it’s needed to keep my app going. I just encountered another (serious) issue when migrating to 3.3.2, which forced me into Blaze 3.0.2 . After a lenghty sessions with Claude, this was the result :

# Iron Router + Blaze 3.0.2 Compatibility Issues & Solutions

This document describes the compatibility issues encountered when upgrading to Blaze 3.0.2 with Iron Router and the solutions implemented.

## 🚨 Root Causes

### Issue 1: Loading Hook Template Rendering Conflict

**Problem**: Iron Router's loading hook in `/packages/ironrouter/lib/hooks.js` was calling `this.renderRegions()` while showing the loading template. This created a conflict with Blaze 3.0.2's template lifecycle when the route later became ready and tried to render templates to the same regions.

**Symptoms**:
- Loading template (spinner) never disappears
- Main content templates don't render
- Browser console shows subscriptions are ready, but UI remains stuck on loading

### Issue 2: Named Yield Regions Not Working

**Problem**: Blaze 3.0.2 has compatibility issues with Iron Layout's named yield syntax (`{{>yield "header"}}`). The regions are created in Iron Router's layout object, but Blaze fails to properly render content to named yield placeholders in templates.

**Symptoms**:
- Header templates render to Iron Router regions but don't appear in the DOM
- `layout.regionKeys()` shows correct regions, but `{{>yield "header"}}` doesn't display content
- Content gets rendered but appears in wrong locations or not at all

## ✅ Solutions Implemented

### Solution 1: Fix Iron Router Loading Hook

**File**: `/packages/ironrouter/lib/hooks.js`

**Change**:
```javascript
// BEFORE (broken in Blaze 3.0.2)
Router.hooks.loading = function () {
  if (this.ready()) {
    this.next();
    return;
  }
  var template = this.lookupOption('loadingTemplate');
  this.render(template || defaultLoadingTemplate);
  this.renderRegions(); // ← This line caused conflicts
};

// AFTER (fixed)
Router.hooks.loading = function () {
  if (this.ready()) {
    this.next();
    return;
  }
  // Blaze 3.0.2 fix: Don't render loading template at all, just wait for ready
  // The subscriptions will still load in the background, and when ready
  // the action function will be called to render the proper templates
};

Result: Eliminates template rendering conflicts while preserving subscription loading behavior.

Solution 2: Replace Named Yields with Direct Template Inclusion

Problem Layout (MenuLayout):

<template name="MenuLayout">
  {{>sAlert}}
  {{>yield "header"}}  ← Broken in Blaze 3.0.2
  <div class="container-fluid p-0 calculatemaximumheight">
    {{>yield}}
  </div>
  {{> tamtamFooter version=versionbuild}}
</template>

Solution Layout (CustomMenuLayout):

<template name="CustomMenuLayout">
  {{>sAlert}}
  {{> header}}  ← Direct template inclusion instead of named yield
  <div class="container-fluid p-0 calculatemaximumheight">
    {{>yield}}
  </div>
  {{> tamtamFooter version=versionbuild}}
</template>

Route Changes:

// BEFORE (broken)
Router.route('/:tenant/login', {
  layoutTemplate: 'MenuLayout',
  action: function () {
    this.render('header', { to: 'header' }); // ← Broken named yield
    this.render('login');
  }
});

// AFTER (fixed)
Router.route('/:tenant/login', {
  layoutTemplate: 'CustomMenuLayout',
  action: function () {
    // Header included automatically in CustomMenuLayout
    this.render('login'); // Only render main content
  }
});

:clipboard: Migration Steps

Step 1: Fix Iron Router Loading Hook

  1. Edit /packages/ironrouter/lib/hooks.js
  2. Comment out this.renderRegions() call in the loading hook
  3. Optionally remove template rendering entirely for cleaner behavior

Step 2: Create Custom Layout

  1. Create CustomMenuLayout template that includes header directly
  2. Keep {{>yield}} for dynamic main content
  3. Import the new layout template

Step 3: Update Controllers

Update all controllers that used MenuLayout:

// Controllers to update:
authenticatedController = baseController.extend({
  layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'
});

authenticatedMgmtController = baseController.extend({
  layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'
});

authenticatedSUController = RouteController.extend({
  layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'
});

authenticatedStatsController = baseController.extend({
  layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'
});

Step 4: Update Individual Routes

Find routes with explicit layoutTemplate: 'MenuLayout' and change to CustomMenuLayout:

# Search for routes to update:
grep -n "layoutTemplate.*MenuLayout" client/main.js

Step 5: Remove Header Render Calls

Remove all this.render('header', { to: 'header' }) calls from route actions since header is now included automatically:

# Find calls to remove:
grep -n "this\.render('header', { to: 'header' })" client/main.js

:test_tube: Testing

Verify Fix Works

  1. Navigate to routes that previously showed endless loading
  2. Confirm header appears correctly
  3. Verify main content renders without conflicts
  4. Test route transitions work smoothly

Layouts That Don’t Need Changes

These layouts work fine with Blaze 3.0.2 (no named yields):

  • LoadLayout - Simple {{>yield}} only
  • RootLayout - Flexbox layout with {{>yield}}
  • NoMenuNoFooter - Minimal {{>yield}}
  • WebSales - Styled {{>yield}}

:bulb: Key Insights

  1. Named yields are the problem: {{>yield "regionName"}} doesn’t work reliably in Blaze 3.0.2
  2. Simple yields work fine: {{>yield}} continues to work normally
  3. Direct template inclusion works: {{> templateName}} is more reliable than named yields
  4. Loading hook conflicts: Template rendering during loading phase conflicts with subsequent renders

:arrows_counterclockwise: Alternative Solutions Considered

Option 1: Fix Iron Layout Package

  • Pros: Would preserve existing route structure
  • Cons: Complex, requires deep understanding of Blaze internals
  • Result: Not attempted due to complexity

Option 2: Downgrade Blaze

  • Pros: Would preserve all existing code
  • Cons: Misses bug fixes and improvements in Blaze 3.0.2
  • Result: Not recommended for long-term maintenance

Option 3: Switch to Flow Router

  • Pros: Modern router designed for current Meteor versions
  • Cons: Major breaking change requiring extensive refactoring
  • Result: Out of scope for immediate fix

:bar_chart: Impact

Routes Fixed: 50+ routes across 4 controllers + individual route declarations
Files Modified:

  • /packages/ironrouter/lib/hooks.js - Core loading hook fix
  • /imports/ui/customlayout.html - New layout template
  • /client/main.js - Route and controller updates

Compatibility: Solution maintains full backward compatibility while fixing Blaze 3.0.2 issues.


This solution was developed and tested with Meteor 3.3.1, Iron Router 2.0.0 (polygonwood:router), and Blaze 3.0.2.

Magnuss
you might be interested in this :

Iron Router + Blaze 3.0.2 Compatibility Issues & Solutions

This document describes the compatibility issues encountered when upgrading to Blaze 3.0.2 with Iron Router and the solutions implemented.

:rotating_light: Root Causes

Issue 1: Loading Hook Template Rendering Conflict

Problem: Iron Router’s loading hook in /packages/ironrouter/lib/hooks.js was calling this.renderRegions() while showing the loading template. This created a conflict with Blaze 3.0.2’s template lifecycle when the route later became ready and tried to render templates to the same regions.

Symptoms:

  • Loading template (spinner) never disappears

  • Main content templates don’t render

  • Browser console shows subscriptions are ready, but UI remains stuck on loading

Issue 2: Named Yield Regions Not Working

Problem: Blaze 3.0.2 has compatibility issues with Iron Layout’s named yield syntax ({{>yield "header"}}). The regions are created in Iron Router’s layout object, but Blaze fails to properly render content to named yield placeholders in templates.

Symptoms:

  • Header templates render to Iron Router regions but don’t appear in the DOM

  • layout.regionKeys() shows correct regions, but {{>yield "header"}} doesn’t display content

  • Content gets rendered but appears in wrong locations or not at all

:white_check_mark: Solutions Implemented

Solution 1: Fix Iron Router Loading Hook

File: /packages/ironrouter/lib/hooks.js

Change:


// BEFORE (broken in Blaze 3.0.2)

Router.hooks.loading = function () {

if (this.ready()) {

this.next();

return;

}

var template = this.lookupOption('loadingTemplate');

this.render(template || defaultLoadingTemplate);

this.renderRegions(); // ← This line caused conflicts

};

// AFTER (fixed)

Router.hooks.loading = function () {

if (this.ready()) {

this.next();

return;

}

// Blaze 3.0.2 fix: Don't render loading template at all, just wait for ready

// The subscriptions will still load in the background, and when ready

// the action function will be called to render the proper templates

};

Result: Eliminates template rendering conflicts while preserving subscription loading behavior.

Solution 2: Replace Named Yields with Direct Template Inclusion

Problem Layout (MenuLayout):


<template name="MenuLayout">

{{>sAlert}}

{{>yield "header"}} ← Broken in Blaze 3.0.2

<div class="container-fluid p-0 calculatemaximumheight">

{{>yield}}

</div>

{{> tamtamFooter version=versionbuild}}

</template>

Solution Layout (CustomMenuLayout):


<template name="CustomMenuLayout">

{{>sAlert}}

{{> header}} ← Direct template inclusion instead of named yield

<div class="container-fluid p-0 calculatemaximumheight">

{{>yield}}

</div>

{{> tamtamFooter version=versionbuild}}

</template>

Route Changes:


// BEFORE (broken)

Router.route('/:tenant/login', {

layoutTemplate: 'MenuLayout',

action: function () {

this.render('header', { to: 'header' }); // ← Broken named yield

this.render('login');

}

});

// AFTER (fixed)

Router.route('/:tenant/login', {

layoutTemplate: 'CustomMenuLayout',

action: function () {

// Header included automatically in CustomMenuLayout

this.render('login'); // Only render main content

}

});

:clipboard: Migration Steps

Step 1: Fix Iron Router Loading Hook

  1. Edit /packages/ironrouter/lib/hooks.js

  2. Comment out this.renderRegions() call in the loading hook

  3. Optionally remove template rendering entirely for cleaner behavior

Step 2: Create Custom Layout

  1. Create CustomMenuLayout template that includes header directly

  2. Keep {{>yield}} for dynamic main content

  3. Import the new layout template

Step 3: Update Controllers

Update all controllers that used MenuLayout:


// Controllers to update:

authenticatedController = baseController.extend({

layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'

});

authenticatedMgmtController = baseController.extend({

layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'

});

authenticatedSUController = RouteController.extend({

layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'

});

authenticatedStatsController = baseController.extend({

layoutTemplate: 'CustomMenuLayout', // Was 'MenuLayout'

});

Step 4: Update Individual Routes

Find routes with explicit layoutTemplate: 'MenuLayout' and change to CustomMenuLayout:


# Search for routes to update:

grep -n "layoutTemplate.*MenuLayout" client/main.js

Step 5: Remove Header Render Calls

Remove all this.render('header', { to: 'header' }) calls from route actions since header is now included automatically:


# Find calls to remove:

grep -n "this\.render('header', { to: 'header' })" client/main.js

:test_tube: Testing

Verify Fix Works

  1. Navigate to routes that previously showed endless loading

  2. Confirm header appears correctly

  3. Verify main content renders without conflicts

  4. Test route transitions work smoothly

Layouts That Don’t Need Changes

These layouts work fine with Blaze 3.0.2 (no named yields):

  • LoadLayout - Simple {{>yield}} only

  • RootLayout - Flexbox layout with {{>yield}}

  • NoMenuNoFooter - Minimal {{>yield}}

  • WebSales - Styled {{>yield}}

:bulb: Key Insights

  1. Named yields are the problem: {{>yield "regionName"}} doesn’t work reliably in Blaze 3.0.2

  2. Simple yields work fine: {{>yield}} continues to work normally

  3. Direct template inclusion works: {{> templateName}} is more reliable than named yields

  4. Loading hook conflicts: Template rendering during loading phase conflicts with subsequent renders

:arrows_counterclockwise: Alternative Solutions Considered

Option 1: Fix Iron Layout Package

  • Pros: Would preserve existing route structure

  • Cons: Complex, requires deep understanding of Blaze internals

  • Result: Not attempted due to complexity

Option 2: Downgrade Blaze

  • Pros: Would preserve all existing code

  • Cons: Misses bug fixes and improvements in Blaze 3.0.2

  • Result: Not recommended for long-term maintenance

Option 3: Switch to Flow Router

  • Pros: Modern router designed for current Meteor versions

  • Cons: Major breaking change requiring extensive refactoring

  • Result: Out of scope for immediate fix

:bar_chart: Impact

Routes Fixed: 50+ routes across 4 controllers + individual route declarations

Files Modified:

  • /packages/ironrouter/lib/hooks.js - Core loading hook fix

  • /imports/ui/customlayout.html - New layout template

  • /client/main.js - Route and controller updates

Compatibility: Solution maintains full backward compatibility while fixing Blaze 3.0.2 issues.


This solution was developed and tested with Meteor 3.3.1, Iron Router 2.0.0 (polygonwood:router), and Blaze 3.0.2.

Hi @polygonwood,
I’ve forked the polygonwood:iron-router package (and its dependencies).

In older Meteor apps that used iron-router, there was a common pattern with contentFor / yield helpers to define and inject content into layouts.

Example:

{{#contentFor "header"}}
  <h1>My Page Title</h1>
{{/contentFor}}

{{> yield "header"}}

The yield part is working fine for me, but I’m having trouble reproducing the contentFor functionality in modern Meteor/Blaze.

Could you please suggest the best way to implement or replace contentFor in this setup?

Thanks!