Building CampaignHawk with Meteor and React (Part 2)

Continuing the discussion from Building CampaignHawk: An Open-Source Election Canvassing App with Meteor and React (Part 1):

Here’s Part 2. This goes over basic file structure, packages, components, and routes.


you’re doing God’s work here. keep it up!
Would also be really interested if you used a redux (flux) workflow for actions. Seeing a medium-sized app come together is 10x more helpful than the small 2-module demos.

1 Like

Thanks for sharing. I really like these kind of insights articles.

Love it.
You’re articles are really helping me raise my game.

Why did you decide to use react-router vs flowrouter?

Great as always! I have a very similar folder structure only for collections I use a both/lib/collections/foo.js so it’s a bit more obvious that it’s sent to both client/server.

Have you thought about models yet? I’ve always rolled my own with Meteor methods and check but the astronomy package looks really nice!

[quote="ssparacio, post:5, topic:9216, full:true"] Why did you decide to use react-router vs flowrouter? [/quote]

I can’t answer for Sam but i’ve used both. react-router is really nice for nested UI’s. At first I thought the nesting was odd, however the nesting will replace FlowRouter’s ‘layout’ and then makes it easier for more complex layouts. It also doesn’t hurt that it’s the defacto router in the React world.

I imagine that react-router will be able to take advantage of the FlowRouter 3 internals if it’s using FastRender under the hood (to bootstrap serverside rendered pages).

I decided on react-router simply because it’s supported by a bigger community.
They both function very well :slight_smile:

1 Like

I haven’t given the models any thought yet, but Astronomy is definitely on my radar! I’ll get to the point where I need to start thinking about it before the end of the week.

I’d be more than happy to go over them with you :slight_smile: Complex models is not a subject in which I have a great deal of experience.

Cool yea let me know… I think we all are just winging it :laughing:

Here’s how I normally roll them so that I can share them across DDP apps easily (I also usually create a facade to make it cleaner like Post.insert(....)):

/*global Mongo, Post:true, Posts:true, User */

// NOTE, doing meteor data/collections this way loses much of Meteor's
// 'magic' and makes more work for us but i'm totally ok trading convenience
// for flexibility and easier to reason with security rules. You can still
// use one liner insert/update methods if you opt into using allow/deny based
// security. Perhaps someone can submit a branch using this methodology too!
// also, becauses i'm lazy, I made a file generator to create the below for you!

var schema = {
  _id: String,
  createdAt: Date,
  updatedAt: Date,
  ownerId: String,
  userName: String,
  desc: String,
  likeCount: Number,
  commentCount: Number

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

// optionally run hook to log, audit, or denormalize mongo data
Posts.after.insert(function (userId, doc) {
  console.log("Inserted Doc", userId, doc);

// Post Model
// Pass the doc id in as the first param, security checks will ensure
// that a user is only allowed to mutate their own document.
// Running these's on the client will *only* run a simulation
// for optimistic UI and the server copy does the realy data mutating.
// This prevents users from tampering data. Trust *nothing* on the client!
// The PostDomain now directly calls the Meteor method instead of having a
// fat model. This approach is more oriented towards Flux and makes it easier
// to reason about data flow. Domains should be the only thing calling these
// model methods (on the client) and the domain method should only be called
// by an action (views never mutate data)
// Example:
//'Post.create' {
//     desc: 'Hello World',
//   });
//'Post.update', '1234', {
//     desc: 'Goodbye World',
//   });
// ** Security README **
// all Post insert, update & delete MiniMongo methods are disabled on the client
// by not having allow/deny rules. This ensures more granular security & moves
// the security logic into the meteor method. all mutation has to happen with
// the Meteor methods. These methods are placed into the 'both' folder so that
// Meteor uses the methods as stubs on the client, retaining the latency
// compensation. if you need to hide the model logic, move the methods into the
// server directory. doing so will lose latency compensation, however a stub
// can be created on the client folder to re-enable latency compensation.

   * Creates a Post document
   * @method
   * @param {object} data - data to insert
   * @param {object} data.desc - post text content
   * @param {object} data.userName - post owner username
   * @returns {string} of document id
  "Post.create": function(data) {
    var docId;
    if (!this.userId) throw new Meteor.Error(401, "Login required");

    data.ownerId = this.userId; // XXX cleanup
    data.createdAt = new Date();
    data.updatedAt = new Date();
    data.likeCount = 0;
    data.commentCount = 0;

    // ensure user doesn't send extra/evil data
    // ignore _id since it's not created yet
    check(data, _.omit(schema, '_id'));

    docId = Posts.insert(data);

    console.log("[Post.create]", docId);
    return docId;

   * Updates a Post document using $set
   * @method
   * @param {string} docId - The doc id to update
   * @param {object} data - data to update
   * @returns {number} of documents updated (0|1)
  "Post.update": function(docId, data) {
    var count, selector;
    var optional = Match.Optional;

    check(docId, String);
    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");
    data.updatedAt = new Date();

    // whitelist what can be updated
    check(data, {
      updatedAt: schema.updatedAt,
      desc: optional(schema.desc),
      commentCount: optional(schema.commentCount),
      likeCount: optional(schema.likeCount)

    // if caller doesn't own doc, update will fail because fields won't match
    selector = {_id: docId, ownerId:};

    count = Posts.update(selector, {$set: data});

    console.log("  [Post.update]", count, docId);
    return count;

   * Destroys a Post
   * @method
   * @param {string} docId - The doc id to destroy
   * @returns {number} of documents destroyed (0|1)
  "Post.destroy": function(docId) {
    check(docId, String);

    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");

    // if caller doesn't own doc, destroy will fail because fields won't match
    var count = Posts.remove({_id: docId, ownerId:});

    console.log("  [Post.destroy]", count);
    return count;

   * Naive implementation of increment like count by 1
   * this will not check for multiple like by the same person or
   * even track who liked it. Perhaps after releasing we can fix this
   * @method
   * @param {string} docId - The doc id to like
   * @returns {number} of documents updated (0|1)
  "": function(docId) {
    check(docId, String);
    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");

    var count = Posts.update({_id: docId}, {$inc: {likeCount: 1} });

    console.log("  []", count);
    return count;

   * Increment a field on Post doc, only allow comments to pass for now
   * @method
   * @param {string} docId - The doc id to like
   * @returns {number} of documents updated (0|1)
  "Post.increment": function(docId, fieldName) {
    check(fieldName, "commentCount");
    if (User.loggedOut()) throw new Meteor.Error(401, "Login required");

    var incField = {};
    incField[fieldName] = 1;
    var count = Posts.update({_id: docId}, {$inc: incField });

    console.log("Post.increment]", count);
    return count;

Hi, Thanks for sharing this! I’m very excited to follow it.

I’m a beginner and I don’t get displayed “this is where the maps go”.
I followed all your steps. I think the problem is with the router because if I don’t use it, I get the App displayed properly.
Do i need to install client NPM packages with browserify, add require and configure it? Or it should work just with the info that is in the second part of your articles?

@norber145 yeah, that part is just placeholder text that I added. I did not include it in the steps. This is what the file should look like :slight_smile:

If you are a beginner, I would highly recommend taking the Discover Meteor course before tackling this one—this series is solidly at an intermediate level.

@samcorcos I know that’s just placeholder text, I’m not that bad, come on haha. My question is , do I need to install client NPM packages with browserify to make react router work?
I will take this tutorial patienly while I’m learning with books, can’t wait!
Thanks for answering

@norber145 Haha sorry about that. I didn’t want to make any assumptions.

You do not need to install browserify because the reactrouter:react-router package already comes with it.

@samcorcos Ok, So I don’t know why I can’t get it! Any idea? I don’t want to give up that soon.

Hmmm that looks ok to me, are there any errors in the console?

Yes, here it is!

Hmm… I don’t know what the issue is. That’s the same code that I have.

I would try reinstalling the package or updating it. If that doesn’t do it, you can run the router without route history.

Have been using the Astronomy Model package for a few months now. We should be ready for production soon so I’ll be able to post more about it soon enough. It’s rather extensive and makes dealing with data in the app very easy. It’s worth taking a look, especially those people that come from a heavy-MVC background.

That being said, we’re using Blaze, not React.

Great job on these detailed posts @samcorcos! Always helpful.

1 Like

I think it might be an issue with 1.0 ver 0.13?.. there are breaking changes in the new 1.0 (and docs need to be set to 1.0 manually)

What does your browserify file look like? (where it requires and exposes)

Here’s how to import history with 1.0:

import { history } from 'react-router/lib/BrowserHistory;

or with require it would be something like:

var history = require('react-router/lib/BrowserHistory')

Where can I find my browserify file?
These are the errors I get.

How can I set 1.0? Where can I find my browserify file?
If not, i will run it without history.