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.