meteor: common mistakes
Over the past few years I've spent countless hours reviewing code and answering questions both on stackoverflow and at the monthly SF devshop. There are certain patterns of mistakes and misunderstandings that most meteor developers (myself included) make on their journey toward learning the framework. In this post I'll catalog some of the most frequent occurrences in the hopes that you can avoid being bitten by at least one of them.
Profile Editing
Do you have any code that looks like this?
if (Meteor.user().profile.isAdmin)
// do important admin things
If so, I've got some bad news for you. Any user can open up a console and run:
Meteor.users.update(Meteor.userId(), {$set: {'profile.isAdmin': true}});
Surprise! User profiles are editable by default even if insecure
has been removed. To prevent this, just add the following deny rule:
Meteor.users.deny({
update: function() {
return true;
}
});
Find and Fetch
If meteor is your first introduction to mongodb, it may be unclear exactly how to read your documents out of a collection. A glance through the docs reveals that there's a function called find. Logically, that's what you'd use to find your documents right? Well, sort of.
find
actually returns a collection cursor, which you can think of as an object used to iterate over and extract mongodb documents. It is not an array of documents.
This is additionally confusing for beginners because template helpers typically use cursors, so most tutorials will focus exclusively on find
.
For those situations where you need to read the documents directly, you'll have to combine find
with fetch like this:
var puppies = Dogs.find({age: {$lt: 1}}).fetch();
Also note that if you ever want to find a single document, you can use the appropriately named findOne.
Published Secrets
Let's say you have a publisher for all of the users in a group:
Meteor.publish('groupUsers', function(groupId) {
check(groupId, String);
var group = Groups.findOne(groupId);
var selector = {_id: {$in: group.members}};
return Meteor.users.find(selector);
});
What's wrong with this (apart from the fact that it isn't reactive)?
Unless you specify which fields are to be returned, mongo will return all of them, which means that clients can see login tokens, encrypted passwords, and anything else stored in your user documents.
You must specify which fields you want when publishing user data in order to prevent leaking your user's secrets. The corrected function could look something like this:
Meteor.publish('groupUsers', function(groupId) {
check(groupId, String);
var group = Groups.findOne(groupId);
var selector = {_id: {$in: group.members}};
var options = {fields: {username: 1}};
return Meteor.users.find(selector, options);
});
Variables as Keys
Building dynamic selectors is extremely common when creating a rich user interface. Imagine a user being able to search a collection of animals by a key (species, size, region, etc.) and a value from a text input. Your code could look something like:
var key = 'species';
var value = 'elephant';
var selector = {key: value};
Animals.find(selector);
But there is a subtle JavaScript gotcha that will prevent this find
from returning the right results:
Using a variable identifier as a key in an object literal will substitute the identifier and not the value.
selector
is actually evaluated as {key: 'elephant'}
and not {species: 'elephant'}
as intended.
In es5, the only way to fix this is to initialize selector
as an empty object and then use bracket notation to set the key:
var key = 'species';
var value = 'elephant';
var selector = {};
selector[key] = value;
Animals.find(selector);
In es6, we can use computed property keys:
const key = 'species';
const value = 'elephant';
const selector = {[key]: value};
Animals.find(selector);
Subscriptions Don't Block
Many aspects of the framework seem like magic. So much so that it may cause you to forget how web browsers work. Take this simple example:
Meteor.subscribe('posts');
var post = Posts.findOne();
The idea that post
will be undefined
is the root cause of roughly one in twenty meteor questions on stackoverflow.
subscribe
works like a garden hose - you turn it on and, after a while, things come out the other end. Activating the subscription does not block execution of the browser, therefore an immediate find
on the collection won't return any data.
subscribe
does, however, have an optional callback which you can use like so:
Meteor.subscribe('posts', function() {
console.log(Posts.find().count());
});
Alternatively, you can call ready
on the subscription handle:
var handle = Meteor.subscribe('posts');
Tracker.autorun(function() {
if (handle.ready())
console.log(Posts.find().count());
});
In more complex apps, your subscription activation code and your data consumption code are often very separate, which reduces the value of the above techniques. Fortunately there are two common ways of tackling the problem:
- If you use iron router, you can wait on subscriptions, prior to rendering the dependent templates.
- You can use guards to check if the data exists within your template code.
I'd recommend using a combination of both approaches in your applications depending on your UI requirements. Waiting on a subscription will force a delay before rendering the page, whereas guards will progressively render data as it's received on the client.
Corollary: In meteor, the majority of "Cannot read property of undefined"errors are caused by an incorrect assumption about the existence of subscribed data.
Overworked Helpers
Helpers act like a rosetta stone for your templates. They can read things like collection documents, session data, and the current template context,then mix it all up and spit out a nicely formatted result.
Notice that I didn't say anything about calling methods, changing state, or posting messages on twitter. This is because helpers are intended to be synchronous translators which are free of side effects.
Because helpers are reactive, developers are often tempted to use them as a shortcut to link changes together. To illustrate this point, here's an example helper which formats a post comment:
Template.postComment.helpers({
message: function() {
var comment = Comments.findOne(this.commentId);
if (Session.get('isExcited')) {
return comment.text.toUpperCase() + '!';
} else {
return comment.text;
}
}
});
Now let's suppose we need to animate the message whenever the comment text changes. We could add some jQuery to our helper, but that makes a big assumption: that the helper only reruns under one condition. What if the session variable changes? What if some other property of the comment gets updated? What if changes in the parent cause postComment
to be re-rendered?
It's safe to assume your helpers will run many more times than you expect them to. For this reason, its never a good idea to use them beyond their intended purpose. Instead you should use tools like autorun and observeChanges to solve these problems.
Note that if your helper needs the result of an asynchronous operation (like a method call) see the answers to this question.
Sorted Publish
When you publish documents to the client, they are merged with other documents from the same collection and rearranged into an in-memory data store called minimongo. The key word being rearranged.
Many new meteor developers have a mental model of published data as existing in an ordered list. This leads to questions like: "I published my data in sorted order, so why doesn't it appear that way on the client?" That's expected. There's one simple rule to follow:
If you need your documents to be ordered on the client, sort them on the client.
Sorting in a publish function isn't usually necessary unless the result of the sort changes which documents are sent (e.g. you are using a limit
).
You may, however, want to retain the server-side sort in cases where the data transmission time is significant. Imagine publishing several hundred blog posts but initially showing only the most recent ten. In this case, having the most recent documents arrive on the client first would help minimize the number of template renderings. [1]
Data Attributes
Remember that hack where you used to stuff all of your application state into the DOM using data attributes? I constantly see code from new users that looks like this:
<template name="nametag">
<div data-name="{{name}}">{{name}}</div>
</template>
Template.nametag.events({
click: function(e) {
console.log($(e.currentTarget).data().name);
}
});
Meteor is here to stop the madness. Because helpers, event handlers, and templates share the same context, the same code should be written as:
<template name="nametag">
<div>{{name}}</div>
</template>
Template.nametag.events({
click: function(e) {
console.log(this.name);
}
});
Caveat: Data attributes may be necessary if you are using a jQuery plugin which requires them.
Dynamically Created Collections
This isn't so much a mistake as it is a line of reasoning that will get you nowhere. Here's an example conversation that I've had many times with new meteor developers:
developer: "I have a collection of diamonds. If a user wants to insert a sapphire, I think I should create a new collection on the fly."
me: "Why?"
developer: "Because diamonds and sapphires could have a slightly different object structure."
me: "What you actually want is a gemstones collection, where each document has a type."
developer: "But they won't have the same schema. Isn't that a problem?"
me: "It doesn't matter. Mongo is a schemaless database."
developer: "Okay, but it's so easy just to declare a new collection on the client."
me: "Sure, but that doesn't do you any good unless the server knows about it as well. The best attempt I've seen is this, but I'm not sure it works."
developer: "Well I guess I cold try that."
me: "How will you inform the other connected clients about which collections exist?"
developer: "Maybe I could publish a collection of dynamically created collections."
me: "Yikes. How would you even query that?"
developer: "Ummmm..."
me: "So how about that gemstones collection?"
developer: "Yes please."
Meteor isn't built for dynamically created collections. Hopefully this will save you a few hours of research.
Merge Box
This is a problem you'll encounter once you start using publications with fields projections. Let's assume a simplified users collection with the following document:
_id: 'abc123',
username: 'bobama',
profile: {firstName: 'Barack', lastName: 'Obama'},
isPresident: true
Imagine there are two publications for users:
return Meteor.users.find({_id: userId}, {fields: {username: 1, profile: 1}});
return Meteor.users.find({_id: userId}, {fields: {'profile.lastName': 1}});
The client can only have one copy of any given document. The server will evaluate which fields should be transmitted to the client, using something called the MergeBox
. In an ideal world, if both of these publishers were active, the client would always have the complete user profile (one sends the full version and the other sends a partial version).
Unfortunately, the MergeBox
only operates on top-level fields, as hinted at by this text in the subscribe documentation:
If more than one subscription sends conflicting values for a field (same collection name, document ID, and field name), then the value on the client will be one of the published values, chosen arbitrarily.
In other words, documents are not deeply merged, and sometimes the complete profile from publication (1) will be chosen, and sometimes the partial version from (2) will be chosen. The client could see a document like this:
_id: 'abc123',
username: 'bobama',
profile: {lastName: 'Obama'}
There are some complicated workarounds to this problem, but the easiest solution is just to use sub-field projections with caution, and to avoid them entirely when you have multiple publishers for the same field.
[1] Thanks to Josh Owens for pointing out this exception.