Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Meteor - how do I make this "reactive" using Deps?

On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).

Drawing the chart is done on Template.rendered method

// client/main.js
Template.listItem.rendered = function() {
    var arr = this.data.userPoints // user points is an array of integers
    $(this.find(".chart")).sparkline(arr);
}

Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.

Meteor.methods({
    "getUserPoints" : function getUserPoints(id) {
        // access some API and fetch the latest user points
    }
});

Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.

Template.listItem.events({
    "click a.fetchData": function(e) {
        e.preventDefault();
        Meteor.call("getUserPoints", this._id);
    }
});

How do I turn this code into a "reactive" one?

like image 243
ericbae Avatar asked Sep 08 '14 11:09

ericbae


2 Answers

You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.

Using ReactiveVar:

if (Meteor.isClient) {
    Template.listItem.events({
        "click a.fetchData": function(e) {
            e.preventDefault();
            var instance = Template.instance();
            Meteor.call("getUserPoints", this._id, function(error, result) {
                instance.userPoints.set(result)
            });
        }
    });

    Template.listItem.created = function() {
      this.userPoints = new ReactiveVar([]);
    };

    Template.listItem.rendered = function() {
        var self = this;
        Tracker.autorun(function() {
            var arr = self.userPoints.get();
            $(self.find(".chart")).sparkline(arr);
        })
    }
}

Using Session:

if (Meteor.isClient) {
    Template.listItem.events({
        "click a.fetchData": function(e) {
            e.preventDefault();
            Meteor.call("getUserPoints", this._id, function(error, result) {
                Session.set("userPoints", result);
            });
        }
    });

    Template.listItem.rendered = function() {
        var self = this;
        Tracker.autorun(function() {
            var arr = Session.get("userPoints");
            $(self.find(".chart")).sparkline(arr);
        })
    }
}

Difference between those implementation :

A ReactiveVar is similar to a Session variable, with a few differences:

ReactiveVars don't have global names, like the "foo" in Session.get("foo"). Instead, they may be created and used locally, for example attached to a template instance, as in: this.foo.get().

ReactiveVars are not automatically migrated across hot code pushes, whereas Session state is.

ReactiveVars can hold any value, while Session variables are limited to JSON or EJSON.

Source

Deps is deprecated, but still can be used.

like image 127
Kuba Wyrobek Avatar answered Nov 08 '22 05:11

Kuba Wyrobek


The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.

userData = new Meteor.Collection(null);

// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
    userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});

Template.listItem.helpers({
    userPoints: function() {
        var pointsDoc = userData.findOne({userId: this._id});
        return pointsDoc && pointsDoc.points;
    }
});

There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:

var pointsDep = new Tracker.Dependency();

// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
    ...
    pointsDep.changed();
});

Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):

<template name="listItem">
    ...
    {{pointsCheck}}
</template>

Template.listItem.helpers({
    pointsCheck: function() {
        pointsDep.depend();
    }
});

Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).

like image 38
richsilv Avatar answered Nov 08 '22 03:11

richsilv