Reactive variables instead of sessions


#1

I’m trying to change my application from sessions to reactive variables, such that the when the page changes the user doesn’t accumulate sessions.

In the application, a session is created for the votes a user made on an item. The items are their own collection and how the user voted on the item is its own collection too. When the items are retrieved, a session of each vote collection is made to produce a reactive frontend (adding classes to the voting buttons). However, instead of using sessions in the template that renders the items, I want to use reactive variables such that it has the same effect as my sessions. I add votes to the template in my body.js template, as below:

function addVotes(element){
  var itemId = element._id;
  Meteor.call('items.uservotes', itemId, function(error, result) {
    if (error) console.log("Error");
    Session.set(itemId, result);
  });
}

I tried adding reactive variables to my items.js, but the classes were only there when refreshing the template. I used the following in that template:

Template.item.onCreated(function itemOnCreate(){
  var self = this;
  self.autorun(function(){
    self.template = Template.instance();
    self.voteVars = new ReactiveVar({});

    Meteor.call('items.uservotes', self.template.data._id, function(error, result) {
      if (error) console.log("Error");
      self.voteVars.set(result);

    });

  });
});

If you need to the other code to evaluate it, then you can check it out on github. Any other suggestions would be appreciated too.


#2

How and where do you access the Session vars? In a helper I presume? And what is the reactivity in the item template that makes you use autorun? Also, self in your code should be the same as Template.instance(), so you can actually use self.data._id.

Check out the following code and see if it makes sense

Template.item.onCreated(function itemOnCreate(){
  var self = this;
  self.voteVars = new ReactiveVar({});

  Meteor.call('items.uservotes', self.data._id, function(error, result) {
      if (error) console.log("Error");
      self.voteVars.set(result);
  });
});

Template.item.helpers({
 voteVars: function() {
  var instance = Template.instance();
  return instance.voteVars.get();
 }
});

#3

Yes, I’m return each variable through their own helper.

Template.item.helpers({
    upvote: function() {
      if (Session.get(this._id) == undefined) {
        return false
      }
      else if (Session.get(this._id).vote == 1)  {
        return true;
      }
        return false;
    }

That code mostly makes sense, but I don’t understand why you have to create an instance variable in the helpers. If you create an instance there, wouldn’t it be a different instance than the one being referenced in the onCreated function? And why wouldn’t you create the same variable in the helpers if they’re equivalent?

The documentation regarding instances and reactivity aren’t very clear to me, so if you know of any resources that describe it, that would definitely help.


#4

In order for the helper to access the template instance which has the ReactiveVar we need to get it first. How else would you be able to reference the templates voteVars?

Exactly the opposite. With Template.instance() I get the actual template instance context in the helper, which is equivalent to the var self = this in the Template.item.onCreated


#5

For some reason I thought you could use the self variable, as though it were accessible throughout the template. Such that you would access it through self.voteVars.get().

In addition to this issue, I’m having a problem with helpers return empty objects. I get two different returns of values for that helper. One that returns empty objects and one with data in the object. Which then gives me an Error: No such function: vote.

I’m trying to access it in the template with {{#if vote "1"}} after {{#with votes}}. The value of the vote variable is numerical, either {-1,0,1}. I tried adding guarding to the helper, but that doesn’t help. For some reason, I imagine you can use wrapAsync() here, but I’m not sure how to appropriately apply it.

And thank you so much for your help.


#6

That is most likely due to the subscriptions not being ready when the helper gets called. Just add debug statements in your helper and you will see. You’re most likely expecting the helper to only run once, but that is usually not the case. If a helper uses a reactive datasource (ReactiveVar, Collection, …) then the helper will get called for every change of the datasource.

If you subscribe in the template.onCreated you usually should include {{#if Template.subscriptionsReady}} so the helpers only get called when there is something to fetch.

I really recommend reading the Meteor Guide


#7

It’s been a long time since I’ve refactored someone else’s code for the fun of it. Here it goes…

Your entire app needs a huge refactor but let’s clean up item with surgical precision (without affecting the rest of the app). I’m going to use ViewModel for this.

First create a client side collection to store user votes and instead of doing Session.set(itemId, result) you would insert into that collection: UserVotes.insert({ itemId: itemId, vote: result })

The items.js code is reduced to this:

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';

import './items.html';

Template.item.viewmodel({
  voteDoc() {
    return UserVotes.findOne({ itemId: this._id() }) || {};
  },
  call(...params) {
    Meteor.call(...params);
  },
  isUser() {
    return Meteor.user() && Meteor.user().username === this.username();
  }
})

The code for itemSettings is axed completely.

Your item & itemSettings templates (in items.html) change to this:

<template name="item">

<tbody>
  <tr>
    <td><span class="vote-count">{{count}}</span>&nbsp;&nbsp;<span style="font-size:18px">{{text}}</span></td>
  </tr>
  <tr class="grey lighten-5">
    <td style="line-height: 10px" class="vote">

      <a {{b "class: { cyan: voteDoc.vote === 1, grey: voteDoc.vote !== 1 }, click: call('items.vote', _id, 1)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-up"></i></a>
      <a {{b "class: { cyan: voteDoc.vote === -1, grey: voteDoc.vote !== -1 }, click: call('items.vote', _id, -1)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-down"></i></a>

      <a style="float: right; margin: 2px" {{b "class: { 'lighten-2': isUser, 'lighten-5': !isUser }, click: isUser && call('items.remove', _id)"}} class="btn btn-small btn-flat grey ">&times;</a>
      <a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': voteDoc.ban }, click: call('items.ban', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-ban"></i></a>
      <a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': voteDoc.star }, click: call('items.star', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-star-o"></i></a>
      <a href="/discuss?area={{_id}}" style="float: right; margin: 2px" class="btn btn-small btn-flat"><i style="font-size:14px" class="material-icons">chat</i></a>

    </td>
  </tr>
</tbody>

</template>

btw, class: { cyan: voteDoc.vote === 1, grey: voteDoc.vote !== 1 } is borderline unruly so one might put that logic into the view model (template code) and not the markup.

<template name="itemSettings">
      <ul id="nav-mobile" class="right hide-on-med-and-down">
        <li {{b "class: { active: area === 'city'}, click: area('city')"}} ><a class="city">city</a></li>
        <li {{b "class: { active: area === 'state'}, click: area('state')"}}><a class="state">state</a></li>
        <li {{b "class: { active: area === 'country'}, click: area('country')"}}><a class="country">country</a></li>
      </ul>
</template>

If you’re interested here’s a quick read @arggh wrote about his experience with ViewModel:
https://medium.com/@arggh/my-experience-with-viewmodel-meteor-d4c89ab7a353


#8

That’s definitely an improvement. I very much appreciate it. However, some stuff still doesn’t work on it.

It mostly works with the exception that the user can still delete an item even if they’re not the user who created it. The classes properly indicate if the user created the item, but it lets the user delete it even when they didn’t create it. Then, I get this error when clicking on the down vote buttons:

viewmodel-parseBind.coffee:17 
Uncaught Error: Unbalanced parenthesis
getMatchingParenIndex @ manuel_viewmodel.js?hash=251fba5…:826
getValue @ manuel_viewmodel.js?hash=251fba5…:862
getValue @ manuel_viewmodel.js?hash=251fba5…:845
(anonymous function) @ manuel_viewmodel.js?hash=251fba5…:969
(anonymous function) @ viewmodel-parseBind.coffee:17
dispatch @ jquery.js:4665elemData.handle @ jquery.js:4333

Also, for whatever reason, it runs the itemVotes() function six times. This seems to be causing issues with my sort on my other template and clicking on the buttons causes items to bounce around and run that function a number of different times (even for items I didn’t click).

Here’s the item.js:

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Mongo } from 'meteor/mongo';

import './items.html';

userVotes = new Mongo.Collection(null);

Template.item.onCreated(function itemOnCreate(){
  var self = this;

  Meteor.call('items.uservotes', self.data._id, function(error, result) {
    if (error) console.log("Error");
       userVotes.insert({itemId: self.data._id, votes: result})
  });

});

Template.item.viewmodel({
    itemVotes() {
      console.log(userVotes.findOne({itemId: this._id()}) || {});
      return userVotes.findOne({itemId: this._id()}) || {};
    },
    call(action, item, value) {
      Meteor.call(action, item, value, function(error,result) {
        if (error) console.log("Error");
        userVotes.update({itemId: item}, {itemId: item, votes: result});
      });
    },
    isUser() {
      return Meteor.user() && Meteor.user().username === this.username();
    }
});

And item.html:

<a {{b "class: { cyan: itemVotes.votes.vote === 1, grey: itemVotes.votes.vote !== 1 }, click: call('items.vote', _id, 1)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-up"></i></a>
<a {{b "class: { cyan: itemVotes.votes.vote === -1, grey: itemVotes.votes.vote !== -1 }, click: call('items.vote', _id, -1)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-down"></i></a>

<a style="float: right; margin: 2px" {{b "class: { 'lighten-2': isUser, 'lighten-5': !isUser }, click: isUser && call('items.remove', _id)"}} class="btn btn-small btn-flat grey">&times;</a>
<a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': itemVotes.votes.ban }, click: call('items.ban', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-ban"></i></a>
<a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': itemVotes.votes.star }, click: call('items.star', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-star-o"></i></a>

#9

Okay, I resolved all the issues except for functions running more than once. I have no idea why, but the call function would not allow a negative number to be passed to it. Instead, I referenced a value defined in the model. I feel like it can still be improved, but it’s so much faster than using reactive variables or sessions.

Here’s the item.js

Template.item.viewmodel({
    increase: 1,
    decrease: -1,
    Item() {
      var votes = userVotes.findOne({itemId: this._id()});
      if (votes !== undefined) {
        return votes;
      }
      else {
        return {
          votes : {
          _id: '',
          item: '',
          username: '',
          vote: 0,
          star: false,
          ban: false
          }
        };
      }
    },
    removeItem() {
      if(this.isUser()){
        this.call('items.remove', this._id());
      }
    },
    upvote() {
      var votes = this.Item().votes;
      if (votes.vote === 1 ) {
        return true;
      }
      else {
        return false;
      }
    },
    downvote() {
      var votes = this.Item().votes;
      if (votes.vote === -1 ) {
        return true;
      }
      else {
        return false;
      }
    },
    call(action, item, value) {
      Meteor.call(action, item, value, function(error,result) {
        if (error) console.log("Error");
        userVotes.update({itemId: item}, {itemId: item, votes: result});
      });
    },
    isUser() {
      return Meteor.user() && Meteor.user().username === this.username();
    }
});

And here’s the item.html

      <a {{b "class: { cyan: upvote, grey: !upvote}, click: call('items.vote', _id, increase)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-up"></i></a>
      <a {{b "class: { cyan: downvote, grey: !downvote}, click: call('items.vote', _id, decrease)"}} class="btn btn-small btn-flat lighten-2"><i class="fa fa-angle-down"></i></a>

      <a style="float: right; margin: 2px" {{b "class: { 'lighten-2': isUser, 'lighten-5': !isUser }, click: removeItem"}} class="btn btn-small btn-flat grey">&times;</a>
      <a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': Item.votes.ban }, click: call('items.ban', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-ban"></i></a>
      <a style="float: right; margin: 2px" {{b "class: { 'cyan lighten-2': Item.votes.star }, click: call('items.star', _id)"}} class="btn btn-small btn-flat"><i style="font-size:14px" class="fa fa-star-o"></i></a>
      <a href="/discuss?area={{_id}}" style="float: right; margin: 2px" class="btn btn-small btn-flat"><i style="font-size:14px" class="material-icons">chat</i></a>

#10

I still cannot possibly recommend ViewModel highly enough. Currently I’m implementing a part of my app that enables users to build customised views (or small web pages if you like) that gather freely customisable data from seemingly random people. It’s sort of… like a combination of Squarespace/Wix and Google forms. With any of the previous tech stacks I’ve been working with, I’d probably have given up already. With ViewModel, it feels like dancing. I’m doing things I didn’t know how to do before ViewModel and Meteor.

This part of my app is built in a very component oriented way with the help of ViewModel and it’s Mixins, Load-functions, Shared properties and whatnot. Now, adding a new customisable element takes ~5 minutes: it basically requires implementing the templates for showing and editing the data and defining the fields to be included. If it wasn’t for the super slow rebuild times of Meteor, I’d be in heaven.

@jwentwistle: I really recommend you to learn VM and make the switch. The only real downside is, it’s basically impossible to go back to plain Blaze (in a psychological way, I mean :slight_smile: ).


#11

With this exercise I discovered a few improvements to the bindings parser. For example, it had trouble with negative numbers in between parenthesis and it didn’t short circuit boolean operators.

Anyway, upgrade to v6.0.0

My original proposal will work with a few minor tweaks:

Since body.js is the one that adds votes I’m creating the UserVotes collection there. This is not where I would put it but it does the minimum “damage” to the structure you have. As a matter of fact I wouldn’t even use the UserVotes collection, I’d use a ViewModel.share for it, but that’s another story.

So modify body.js:

Add the UserVotes collection:

export const UserVotes = new Mongo.Collection(null);

And as mentioned before, insert into UserVotes instead of the Session variable:

function addVotes(element){
  var itemId = element._id;
  Meteor.call('items.uservotes', itemId, function(error, result) {
    if (error) console.log("Error");
    UserVotes.insert({ itemId: itemId, vote: result })
  });
}

Finally items.js needs to import UserVotes:

import { UserVotes } from './body.js';

Let me know how it goes.


#12

That’s good to hear. And yeah, I intended for the userVotes to be used within each of the items because the items templates are rendered on other templates and I didn’t want to have to subscribe to the server-side votes collection with each of those templates.

Otherwise, you did a really good job with this package. I’m trying to think of other way to optimize my application with it. And thanks again!


#13

Okay, I have another related question. I have another template that I want to implement a meteor call in view model, but I don’t want to use a collection. I tried to use reactive variables with view model and that didn’t work either. I’m trying to do the following, but the this.variable() scopes to the Meteor.call() function instead of the view model property.

variable: false,
call(action, item, value) {
      Meteor.call(action, item, value, function(error,result) {
        if (error) console.log("Error");
        this.variable(result);
      });
    }

#14

You need to get a reference to “this” before making the call (by convention the variable is called “that” or “self” ) or use es6 fat arrow. Search for “Javascript this that” and “es6 fat arrow” for more information.