Mathieu Bouchard’s collection-hooks is one of the most popular packages out there, with over 25k installs.

It’s one of these small utilities that does only one thing, but does it very well: in this case, expose hooks that can be called before and after every insert, update, remove, find, and findOne.

Using Collection Hooks

The syntax is fairly straightforward. For example, here’s how you’d add a createdAt timestamp to every new document:

Posts = new Mongo.Collection("posts");

Posts.before.insert(function (userId, doc) {
  doc.createdAt = Date.now();
});

One thing to note is the lack of a return statement. In JavaScript, objects are passed by reference, not by value. In other words, when you write doc.createdAt = Date.now();, you’re modifying the original doc object the function was called on.

So what can collection hooks be used for? Let’s look at a couple examples.

Denormalizing Data

One of the most common use cases for collection hooks is denormalizing data.

A simple denormalization example would be copying a post’s author’s name from the author’s user object to the post object itself.

So this lets you get the author’s name with:

post.authorName;

Instead of the much more complex:

Meteor.users.findOne(post.authorId).name;

(And that’s not even mentioning the fact that you’ll have to publish the Meteor.users collection separately.)

But what happens when the author decides to change their name? Won’t our data get out of sync? Have no fear, collection hooks to the rescue!

Meteor.users.after.update(function (userId, doc, fieldNames, modifier) {
  if (doc.name !== this.previous.name) {
    Posts.update({authorId: doc._id}, {authorName: doc.name}, {multi: true});
  }
});

If we detect any changes to the name, we’ll simply run a query to change the authorName property of every post authored by this user.

Change We Can Believe In

In a previous version of this article, we were testing for field changes with:

if (modifier.$set && modifier.$set.name) {
 ...
}

As David Weldon pointed out, this is ambiguous because the change might not always happen through Mongo’s $set operator (see also the “Operator Confusion” section below).

Fortunately, the collection-hooks package exposes a this.previous object containing a copy of the document before any modifications, which we can compare with doc (the new document) to find out if the property changed.

Chain Deletion

Now let’s say our post author gets fed up with your app and decides to delete their profile, along with all their content. You can use collection hooks to implement a chain deletion procedure and delete all their posts in one go:

Meteor.users.after.remove(function (userId, doc) {
  Posts.remove({authorId: doc._id});
});

Deleting content willy-nilly can have unexpected consequences though (for example, what happens to these post’s now orphaned comments?), so maybe you want to be a little more subtle about it and not actually delete the posts.

Instead, you could set a deleted flag to true:

Meteor.users.after.remove(function (userId, doc) {
  Posts.update({authorId: doc._id}, {$set: {deleted: true}}, {multi: true});
});

Sanitizing User Input

Here’s another common scenario: you want to let users enter HTML in a text field, so you output that field using Spacebar’s triple-brace syntax ({{{myField}}}), which outputs a variable as HTML.

Problem: you’ve just opened up a huge security hole, as nefarious hackers can now get arbitrary JavaScript code to run on your own site.

Pete Corey has a great article that explains this vulnerability in more details, and suggests sanitizing any user input to avoid the problem. But how do you do that exactly?

We could take the user’s submission, remove any disallowed tags, and only then store the result in the database. But by doing this we’re losing the user’s original content in the process. If someone’s trying to hack our site, we probably want to keep a trace!

So we could do the sanitizing when displaying the content. But that means running our sanitizing algorithm over the exact same content every single time someone displays a page, which is inefficient to say the least.

The solution: we’ll store two fields, htmlInput and htmlOutput. And thanks to collection hooks, keeping them in sync just takes a few lines of code:

Posts.before.insert(function (userId, doc) {
  if (Meteor.isServer && doc.htmlInput) {
    doc.htmlOutput = Sanitize(doc.htmlInput);
  }
});

Posts.before.update(function (userId, doc, fieldNames, modifier) {
  if (Meteor.isServer && this.previous.htmlInput !== doc.htmlInput) {
    modifier.$set.htmlOutput = Sanitize(doc.htmlInput);
  }
});

Note the Meteor.isServer check. If htmlOutput was directly modifiable from the client, it would be easy to simply bypass the sanitization. So you’ll want to set up your Allow/Deny rules accordingly, and make sure the hook only tries to run on the server.

Deciding what runs on the client and what runs on the server is a key part of properly setting up your hooks, as we’ll soon see.

Also note that you can see this exact pattern in action in Telescope.

User Post-Create Hook

Meteor exposes a Accounts.onCreateUser hook, but that runs before user creation. In other words, at that point the user object doesn’t exist yet in the database.

So collection hooks is a handy way to modify a user right after it’s been created.

For example, Telescope uses Users.after.insert hook to make API calls to subscribe new users to a Mailchimp newsletter or check if they were invited to the site by another user.

By doing this in a hook instead of in Accounts.onCreateUser, we’re able to do all these operations in an asynchronous manner that doesn’t hold back the rest of the user creation process.

Bypassing Hooks

It’s also important to mention that hooks can be bypassed using the direct keyword.

For example, if we wanted to update a timestamp every time a post is viewed without triggering any hooks, we could do:

Posts.direct.update(id, {$set: {lastViewedAt: new Date}});

This makes sense for operations that will need to run a lot. If our post’s lastViewedAt timestamp is going to be updated multiple times per second, it’s good to have a way to avoid rerunning all our hook chain every single time.

Collection Snags

While collection hooks are a great addition to your Meteor toolbelt, it’s also important to keep a few things in mind.

Collection hooks run after every operation. So if you add an update hook expecting it to run after a user edits a post’s content, remember that it will run after any operation that modifies the post object, such as that post being voted or commented on.

This is why it’s so important to test which property is being modified, to avoid running hooks unnecessarily or triggering errors.

Operator Confusion

This can be easier said than done though, as Mongo supports other operators beyond the usual $set, making it sometimes tough to pinpoint the correct property.

This is especially true in large apps, where adding too many hooks to the same operations can quickly lead to a situation where you have random disconnected bits of code running every time you insert or update a document, making your app’s overall logic harder to grasp (especially since it can be difficult to know in which order hooks will run).

So it might be a good idea to keep all of a collection’s hooks in the same file (for example, wherever you declared your collection and schema), or at least force yourself to follow a consistent structure.

Client vs Server

As usual with Meteor code, it’s also important to think about whether your code should run on the client as well. As we’ve seen in the sanitizing example, running a hook on the client is sometimes not possible (if it affects properties that shouldn’t be modifiable directly).

One approach, recommended by Meteor Casts, is to split up your client-side and server-side hooks. But by doing so, you’ll be foregoing the benefits of latency compensation (also known as “optimistic updates”),

To get a definitive answer, I decided to go straight to the source and ask package creator Mathieu Bouchard what he advised. He too recommended paying attention to where you run your hooks:

I suggest users start with server-only hooks and only share on the client when latency compensation is appropriate. So only share hooks for those “createdAt” type operations, or those that auto-assign “ownerId” etc. Hooks that do cascade-style deletions, and pretty much all other types of hooks are better off server-only.

Performance Concerns

One of the things to keep in mind with collection hooks is that in order to supply a previous object in the update hook, the package does a findOne on the document being modified before updating it.

This is obviously very convenient in many cases, but could be a problem if you are doing frequent updates on a document (such as voting, or updating a lastViewed timestamp).

To avoid this performance cost, you can pass the fetchPrevious: false option when defining your hook:

Posts.after.update(function (userId, doc, fieldNames, modifier, options) {
  // ...
}, {fetchPrevious: false});

But the fact remains that if one of your hooks does need to fetch the document, it’ll have to do it on every operation whether the hook itself does something or not.

Hooks & Methods

Hooks also become a little less useful if you’re moving away from Allow/Deny and towards Meteor methods.

After all, a method can run any arbitrary block of code. So if you’re using a method, you might as well put your denormalization/chain deletion/sanitization code right there where you can see it, instead of splitting it to another part of your codebase.

Then again, you can always start with hooks, and switch to methods later on if need be. When you end up:

  • Having to use more than two or three collection hooks for the same operation.
  • Needing to limit your hook by using multiple if conditions.
  • Bypassing your hooks with the direct keyword too often.

It might be a sign that you need to reconsider your logic and maybe put it in a method instead.

Conclusion

Similarly to Allow & Deny, hooks are a simple concept that can quickly become very complex if not implemented properly.

So like most things they’re best used in moderation, and hopefully this guide will help you make the most of them without getting hooked!