is passing ractive as an argument good practice? - ractivejs

Let's say, instead of packing your onInit and onRender sections of your Ractive declaration in your index.html with a lot of functions that need access to the Ractive object, you wish to keep the index.html cleaner and simpler and reference functions in libraries in other files.
Is there any harm in passing Ractive itself as an argument, so these "external" functions can access it?
For example, instead of:
oninit: function() {
// tons of code here
}
doing this?
oninit: function() {
doThisThing(ractive)
}
Then, in a separate file:
function doThisThing(ractive) {
pingAnAPI(function(response) {
ractive.set('data', response);
)};
}
Just wondering if there would be memory issues or any other undesirable effect if you did this a lot.
Thanks, Ractive is awesome!

doThisThing appears to be your data layer, and should not be aware of your UI layer. Otherwise, you'd risk tightly coupled code.
Instead of breaking out the code and passing the ractive around, break away your data fetching logic. Call the API, have it return a promise. Ractive can hold on to that promise.
// On the component
oninit: function() {
var instance = this;
pingAnAPI().then(function(response){
instance.set('data', response);
});
}
// On your data layer
pingAnAPI: function(){
return $.get('path/to/endpoint');
}
Another way you can do it, since you're considering separate files, is to break out from index.html and use component files. Read the component spec for authors page for more details.
As for memory issues, I'd not worry about that as early as now. Code maintainability should be your first agenda.

Related

How to split code to functions in meteor templates

In my project one of the templates has in it's onRendered method more than 250 lines. Code is becoming more and more unreadable and unmaintainable (because of its monolithic) and i want to split all the code to functions to achieve something like this:
Template.Products.onRendered(function () {
initCarousels();
const allProducts = Meteor.call('server/collections/products::getAll', product._id, (err, content) => {
...
});
const sortedProducts = sortProducts(allProducts);
updateCarousels(sortedProducts);
...
this.autorun( () => {
this.subscribe('products');
...
if (this.subscriptionsReady()) {
...
}
});
}
Of course, i can define functions in onRendered method, but declaring them in onRendered and using in the same place seems not perfect way do do this - i'm searching something like moving functions from templates or even moving them to another file. Can you advise me a way to achieve this?
Yes! you can move code to lib/ directory and use which will be sibling of Client and server folders.
Template.registerHelpers(function_name, function());
When the server starts it will first load lib/*.js files alphabetically, so you can use the files from client.

getMeteorData race conditions with component lifecycle?

I'm getting some pretty undesirable behavior in my app, and I'm having a hard time replicating the issue and/or figuring out what I'm doing wrong or not understanding about React that's causing my components to act this way.
What I want to do is to get some data from Mongo on the App component, then have all of that data readily available for any child that I want.
<App> //get data here, pass to children through props
<ChildElement1 data={this.data.appData}/>
<ChildElement2 data={this.data.appData}/>
<ChildElement3 data={this.data.appData}/>
</App>
Here's how I've attempted to tackle this with React so far:
App = React.createClass({
mixins: [ReactMeteorData],
getMeteorData() {
let _id = 'exampleId';
return {
appData: Collection.findOne({_id})
};
},
render() {
return (<ChildElement1 data={this.data.appData} />);
}
});
ChildElement1 = React.createClass({
getInitialState() {
return {
values: ['val1', 'val2', 'val3', 'val4'] //default values
};
},
componentWillMount() {
if(this.props.data.specificValues) {
this.setState({values: this.props.data.specificValues});
}
},
render() {
let values = this.state.values;
return (<span>{values[0]} {values[1]} {values[2]} {values[3]}</span>);
}
});
So here's where it gets weird. When I call componentWillMount(), sometimes this.props.data is defined and other times it's not, which leads me to believe there's some sort of race conditions going on where sometimes that data gets loaded correctly as a prop and other times it doesn't.
I then figured that, well okay, I can't depend on the data prop being there before the component is initially mounted, so I could instead use componentWillReceiveProps(nextProps) and check the updated props that way (and update the state, if necessary). HOWEVER! After using componentWillReceiveProps, now this.props.data is seemingly ALWAYS correctly attached to the props of ChildElement1 (which means componentWillReceiveProps doesn't run!).
My final solution was to use BOTH componentWillMount and componentWillReceiveProps to account for both situations and to do the exact same check in both locations. This fix works, but boy does it seem messy and probably indicates a lack of understanding of component lifecycles, how the meteor/react should properly interact, both, or something else completely.
I'd sure appreciate a bit of help here.
edit: I've come up with a small improvement - instead of using componentWillMount and componentWillReceiveProps to do the check to see if there are specific values defined in the Mongo Collection, I put that logic in render like so:
render() {
let data = this.props.data,
values = (data) ? data.specificValues : this.state.values;
return (<span>{values[0]} {values[1]} {values[2]} {values[3]}</span>);
}
There's definitely still some sort of underlying issue, however, as I still don't understand why this.props is so inconsistent when given data retrieved from getMeteorData. This version is a bit more succinct, however.
I found a better approach to this rather than passing the data returned from getMeteorData to each of the children as props. Using the methods described here: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html, I explicitly listed the childContextTypes and getChildContext in <App /> and then contextTypes in <ChildElement1 />, which allows this.data.appData to be available by way of this.context in <ChildElement1 /> and presumably within any other children of <App />. Although I gotta admit, declaring every single proptype of the collection is a major PITA, seems like it'd be necessary to write a mixin (or rather, a bunch of mixins) to handle that stuff.

Meteor performance: not sure if publication is causing the lag

My Meteor app runs slowly in the beginning for about ten seconds, and then becomes fast again. I am trying to improve the performance but having troubles to find the real cause.
I thought the problem was that I am publishing all the course information like following:
if (Meteor.isServer) {
Meteor.publish("courses", function() {
return Courses.find();
});
}
I tried using Kadira to monitor exactly what's happening. However, looking at the result, I am starting to think maybe it's not the real problem.
If it only takes 292ms for pubsub response time, it shouldn't feel that laggy but I cannot think of any other reason why the app would be so slow in the beginning and become fast again. Can an expert point me to the redirection?
UPDATE:
I could improve the duration of lagginess in the beginning by making the following changes:
in /server/publications.js
if (Meteor.isServer) {
Meteor.publish("courses", function() {
// since we only need these two fields for the search bar's autocomplete feature
return Courses.find({}, {fields: {'catalog':1, 'titleLong':1}});
});
Meteor.publish("courseCatalog", function(catalog) {
// publish specific information only when needed
return Courses.find({"catalog": catalog});
});
}
and in router.js I made changes accordingly so I subscribe based on specific pages. But there's still some lag in the beginning and I wonder if I can make more optimizations, and what is the real cause of the slowness in the beginning.
UPDATE2:
I followed the suggestion and made changes like below:
Session.set('coursesReady', false); on startup.
and in router:
Router.route('/', function () {
Meteor.subscribe("courses", function(err) {
if (!err) {
console.log("course data is ready")
Session.set('coursesReady', true);
}
});
....
and in /lib/helpers.js which returns data for typeahead library
if (Meteor.isClient) {
Template.registerHelper("course_data", function() {
console.log("course_data helper is called");
if (Session.get('coursesReady')) {
var courses = Courses.find().fetch();
return [
{
name: 'course-info1',
valueKey: 'titleLong',
local: function() {
return Courses.find().fetch();
},
template: 'Course'
},
But now the problem is that when the helper function is called, the data is never ready. The console print:
Q: How do I ensure that the helper function is called only after the data is ready, OR called again when the data is ready? Since Session is reactive, shouldn't it be called again automatically?
I can't check this right now, but I believe your issue might be that the course_data helper is being run multiple times before all 1000+ documents in the subscription are ready, causing the typeahead package to re-run some expensive calculations. Try something like this:
/client/views/global/helpers.js
Template.registerHelper("course_data", function() {
if (!Session.get('coursesReady')) return [];
return [ //...
/client/subscriptions.js
Meteor.subscribe("courses", function(error) {
if (!error) Session.set('coursesReady', true);
});
Update:
Really, Meteor's new features this.subscribe() and Template.instance().subscriptionsReady() are ideal for this. Session isn't really the right choice, but it should still be reactively updating (not sure why it isn't for you). Try instead making the following changes to /client/views/navwithsearch.js (and main, though ideally both templates should share a single search template):
Template.NavWithSearch.onCreated(function() {
this.subscribe('courses');
});
Template.NavWithSearch.onRendered(function() {
this.autorun(function() {
if (Template.instance().subscriptionsReady()) {
Meteor.typeahead.inject();
}
});
});
The idea is to tie the lifecycle of the subscription to the view that will actually be using that subscription. This should delay the typeahead injection until the subscription is completely ready.

Template empty initially but renders properly on changing and coming back to route

I have a template named profile which contains three other templates. One of these templates is {{> postlist}}
and the helper function for this template is
Template.postlist.helpers({
posts: function() {
return Posts.find({rph: {$in : postsArr}});
}
});
The problem is on going to the route, postlist template is empty, since postsArr is calculated later after the dom has loaded on the basis of other two templates. But, if I click on other route and come back to this route, the template renders properly.
What should I do that template renders properly initially itself?
The easiest way would be to us Session, though it's probably the worst option:
Template.postlist.helpers({
posts: function() {
return Posts.find({rph: {$in : Session.get('postsArr') }});
}
});
If you now call Session.set('postArr', ...) anywhere in your code the posts helper will update automatically. The second option is to use a shared reactive variable:
var postsArr = new ReactiveVar();
and then inside your helper:
return Posts.find({rph: {$in : posts.Arr.get() }});
Now you can do postsArr.set(...) and everything should work fine. Just remember to meteor add reactive-var do your project.
One last doubt is: where to put that reactive variable declaration? In most cases you can do away with putting in a single "controller" file. It will work as long as:
- you only have one instance of your template a time
- the code which sets ad gets the value of you reactive variable may be put in the same file
If one of the above conditions does not hold, then the only option to go, which is BTW the best possible, is to put your state variable in your template's scope. This is how you do it:
Template.postsList.created = function () {
this.postsArr = new ReactiveVar();
};
Template.postlist.helpers({
posts: function() {
return Posts.find({rph: {$in : Template.instance().postsArr.get() }});
}
});
From helpers you can always access postsArr using the Template.instance() routine which always return the current template instance, for which the helper was called. From event handlers, note that the second argument of your handler is always the template instance, which you're interested in.
If you need to access it from another templates, then you should probably put your state variable on the corresponding route controller. Assuming you're using iron-router, that would be:
Iron.controller().state.get('postsArr');
The Iron.controller routine grants you access to the current route controller. Read this for more details.

Render a Backbone.js collection

I am a Backbone.js n00b and trying to get my head around it. I know how to render a model using a view and the built-in underscore.js templating engine. Now I'm trying to render a collection and that's where I get stuck. There is no server here, so I'm not fetching anything remotely, just a simple HTML page with some JavaScript.
ContinentModel = Backbone.Model.extend({});
ContinentsCollection = Backbone.Collection.extend({
model: ContinentModel,
initialize: function () {
this.continentsView = new ContinentsView;
this.bind("reset", this.continentsView.render);
}
});
ContinentsView = Backbone.View.extend({
el: '#continents',
template: _.template($('#continents-template').html()),
render: function() {
var renderedContent = this.template(this.collection.toJSON());
$(this.el).html(renderedContent);
return this;
}
});
$(function() {
var continentsCollection = new ContinentsCollection();
continentsCollection.reset([{name: "Asia"}, {name: "Africa"}]);
});
It breaks on the template attribute line in the view but I'm not sure that's where I need to look. Am I supposed to render a collection or do I miss the point completely here (maybe collections are just grouping objects and I shouldn't look at it as a list I can render)?
Thanks for helping...
The problem is that when you define ContinentsView, the template is evaluated and it uses $('#continents-template') - but the DOM is not ready yet, so it does not find the template.
To solve it, simply move the template assignment in the initialize function:
ContinentsView = Backbone.View.extend({
el: '#continents',
initialize: function() {
this.template = _.template($('#continents-template').html());
}
...
Regarding collections, yes, they are grouping objects, specifically sets of models.
You should make the code so the models (and collections) do NOT know about the views, only the views know about models.
ContinentModel = Backbone.Model.extend({});
ContinentsCollection = Backbone.Collection.extend({
model: ContinentModel,
// no reference to any view here
});
ContinentsView = Backbone.View.extend({
el: '#continents',
initialize: function() {
this.template = _.template($('#continents-template').html());
// in the view, listen for events on the model / collection
this.collection.bind("reset", this.render, this);
},
render: function() {
var renderedContent = this.template(this.collection.toJSON());
$(this.el).html(renderedContent);
return this;
}
});
$(function() {
var continentsCollection = new ContinentsCollection();
continentsCollection.reset([{name: "Asia"}, {name: "Africa"}]);
// initialize the view and pass the collection
var continentsView = new ContinentsView({collection: continentsCollection});
});
It is also worth noting there are additional complexities that quickly rear their heads when rendering a collection in a view. For instance, the view generally needs to be re-rendered when models are added or removed from the collection. It isn't rocket science to implement your own solution, but it is probably worth looking into existing solutions since there are quite a few tried and tested ones out there.
Backbone.CollectionView is a robust collection view class that handles selecting models in response to mouse clicks, reordering the collection based on drag and drop, filtering visible models, etc.
Several popular frameworks built on top of backbone also provide simple collection view classes, like Backbone.Marionette, Chaplin, and Layout Manager.
Even though Backbone itself does not provide any structure for rendering a collection, it is a non-trivial problem and lots of people have different opinions on how it should be done. Luckily it is such a common need that there are quite a few good options already in the eco system.

Resources