How to use Meteor collections pub/sub with react hooks

clean wrapping of meteor subscription data with the useTracker hook

Let's start with a simple collection (for example: /imports/api/articles/index.ts)

import { Mongo } from 'meteor/mongo';

export type Article = {
  slug: string,
  title: string,
  description: string,
  text: string,
}

export const ArticlesCollection = new Mongo.Collection<Article>('articles');

// security best practice: deny all client-side updates by default
ArticlesCollection.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});

Now define some server-side publications (for example: /imports/api/articles/server/publications.ts).

import { Meteor } from "meteor/meteor";
import { ArticlesCollection } from "..";

Meteor.publish('articles', () => {
  return ArticlesCollection.find({})
});

Meteor.publish('article', ({ slug }) => {
  return ArticlesCollection.find({ slug })
});

Remember that you have to include this in the actual server code, like in /server/main.ts:

import '../imports/api/shared/articles/server/publications';

Now to the interesting part, let's define two hooks to actually fetch the data (for example: /imports/api/articles/client/hooks.tsx)

import { Meteor } from "meteor/meteor";
import { useTracker } from "meteor/react-meteor-data";
import { Article, ArticlesCollection } from "..";

export const useArticle = (slug: string): Article | undefined => {
  return useTracker(() => {
    const sub = Meteor.subscribe('article', { slug });
    if (sub.ready()) {
      return ArticlesCollection.findOne({ slug })
    } else {
      return undefined
    }
  }, [slug])
}

export const useArticles = (): Article[] => {
  return useTracker(() => {
    const sub = Meteor.subscribe('articles');
    if (sub.ready()) {
      return ArticlesCollection.find().fetch()
    } else {
      return []
    }
  })
}

Note that the last argument of the useTracker hook (similar to useEffect) does declare a reactive dependency on the slug in the first hook. It may be undefined on the first run (like, when you fetch the slug from react-router with a hook first).

Now rendering some react pages is as easy as this example:

import React from 'react';
import { useParams } from "react-router-dom";
import { useArticle } from '/imports/api/articles/client/hooks';
import { Template } from '/imports/ui/templates/ArticleTemplate';

export default () => {
  const { slug } = useParams<{ slug: string }>();
  const article = useArticle(slug);
  if (article) {
    return (<Template article={article} />)
  } else {
    return (<div>loading data...</div>)
  }
};

Technologies: