Last Updated: February 25, 2016
·
812
· harinsa

Client Reactive Join in Meteor

I ran into this issue while trying to create a simple blogging feature on Meteor, where I have 2 collections related to each other. One is called news , and the other called images. Each news will have an imageId referencing the image associated with it. I used CollectionFS for the images collection.

At first I did a naive way of publishing the news with the image. Where I query the list of imageIds from the news and return Images corresponding to those ids. Here is my code:

Meteor.publish('newsWithImage', function(options) {
if (options) check(options, { limit: Number });
    else options = { limit: 5 };
    options.sort = { createdAt: -1 };
    var news = News.find({}, options);
    var imageIds = news.map( function(news) { return news.image_id });
    // Naive join
    // if image is updated, news won't update the new image until reload.
    return [
        news,
        Images.find({_id: {$in: imageIds}})
    ];
});

The problem is, whenever I uploaded a new post, only the news collection on the client gets the update, while the image remain unchanged. As a result, the new post would show up with a empty space where the image belongs. This happens because the publish method on the server is not reactive and won't recompute imageIds when the news changes.

The solution is to compute the imageIds on the client instead so that whenever the imageIds array change, the image subscription will get updated.

So I breakup the publish into two methods; one for the news and one for images with an array of ids as a parameter.

On the server:

 // server/publications.js
Meteor.publish('news', function(options) {
    if (options) check(options, { limit: Number });
    else options = { limit: 5 };
    options.sort = { createdAt: -1 };
    var news = News.find({}, options);
    return news;
});

Meteor.publish('imageWithIds', function(imageIds) {
    return Images.find({_id: {$in: imageIds}});
});

On the client:

Template.newsList.onCreated(function () {
  var instance = this;
  // autorun for news
  instance.autorun( function (){
    //Some setup code ...
    instance.subscribe('news', options);
  });
  // autorun for images
  instance.autorun( function() {
    var imageIds = News.find().map( function(p){ return p.image_id });
    console.log(imageIds);
    instance.subscribe('imageWithIds', imageIds);
  });
});

Here I am doing the subscription at a template level ( you can learn more about it here ). instance.autorun is similar to Tracker.autorun, except it is also specific to this template. What autorun does is, it re-runs the function you pass to it every time the data inside the function changes.

So when I insert a new news, the imageIds in the image autorun would change ( increase by one value ). This would cause the function inside the autorun be re-run, and the subscription for the image to be updated.

*Note that the subscription of the 'news' publication probably doesn't have to be inside the autorun method in normal case, but here my options parameters is reactive, so I want to update the subscription whenever options changes.

If you want to get a more complete overview of joining collections reactively, check out Sasha Grief's article on Discover Meteor.