Has createContainer performance issues?

Hey guys,
just have a question to Meteor’s createContainer. For example, I’m using this kind container:

let container = createContainer((props) => {

const songs = Songs.find({playlistId:props.params.playlistId}).fetch();
const videos = Videos.find({playlistId:props.params.playlistId}).fetch();
const project = Project.findOne({_id: props.params.id});

return {
    project,
    songs,
    videos

};
}, Playlist);

I’ve tried to check the properties within componentDidUpdate on the child component (Playlist).

componentDidUpdate(pp,ps) {
  console.log(this.props.videos == pp.videos);
}

I get a false, even if only the Song collection has changed and the Video data stays the same. So I’m wondering if this isn’t a performance issue for React or the garbage collector? It seems like that the Tracker component redefines all variables on every single change and not only the tracker source which has changed.

That is correct, tracker runs your function whenever any of the dependencies changes (i.e. the reactive functions called).

Consider breaking it into smaller components, e.g.


<SongList playlistId={playlistId} />
<VideoList playlistId={playlistId} />
<ProjectDetails projectId={projectId} />

1 Like

Thank you for your reply. I’m just wondering if a solution with react-komposer would be more efficient. I’ve tried this code and it seems like the unchanged Tracker variables stay the same, after one Tracker dep has changed.

var testy1 = new ReactiveVar("hello");

const trackerContainer = composeWithTracker((props, onData) => {


    let test1, test2 = null;



    Tracker.autorun((tracker) => {

        test1 = testy1.get();
        if(test1) {
            onData(null,{test1,test2});
        }
    });


    Tracker.autorun((tracker) => {

        test2 = new ReactiveVar(Math.random());
        if(test2) {
            onData(null,{test1,test2});
        }
    });



})(Start);

You can’t compare two objects like this, the result will always be false. But I wouldn’t recommend doing a deep comparison either, it’s too expensive. Figure out exactly what you’re trying to check for changes, and refine your code.

But in my react-komposer example above it is true (this.props.test1 == pp.test1 if I change test2). I’m just wondering why Meteor’s container doesn’t use a single autorun for each Tracker dependecy and only updates the data which has changed instead of sending everything as new to React.

I’m just thinking about a collection which may return 100 items and another collection which returns 1 doc. If the 1 doc changes, the other 100 docs will be send as “new” to React and I guess they will trigger a render() if you didn’t change shouldComponentUpdate.

As @macrozone said, in this case we have to create a new component which only subscribes to this 100 items.

It’s not sending everything as new. Ok, here’s a quick rundown on createContainer:

We’ll make a container for MyComponent. It looks like this, as an empty container that does nothing at the moment, but just to illustrate the reactivity of it:

import { createContainer } from 'meteor/react-meteor-data';
import MyComponent from './MyComponent';

export default createContainer(() => {
  /*
   * Anything reactive (Mongo, Session, ReactiveVar, etc) in this function will
   * cause it to re-run and pass props into MyComponent.
   */
  return {}
}, MyComponent);

Now let’s say I’m going to pass two collections into MyComponent:

import { createContainer } from 'meteor/react-meteor-data';
import MyComponent from './MyComponent';
import { Orders } from '/imports/collections/orders';
import { Items } from '/imports/collections/items';

export default createContainer(() => {
  /*
   * Anything reactive (Mongo, Session, ReactiveVar, etc) in this function will
   * cause it to re-run and pass props into MyComponent.
   */
  const shopDataHandle = Meteor.subscribe('shopData');
  const orders = Orders.find().fetch();
  const items = Items.find().fetch();

  return {
    isDataReady: shopDataHandle.ready(),
    items,
    orders,
  }
}, MyComponent);

We hit our first render, and all the data is displayed as expected. Now let’s say the Items collection is updated. This will trigger a re-render. With createContainer, you want to use MyComponent's componentWillReceiveProps lifecycle method to handle data updates.

class MyComponent extends Component {
  componentWillReceiveProps(nextProps) {
    if (this.props.items.length !== nextProps.items.length) {
      this.setState({ itemCount: nextProps.items.length });
    }
  }
}

In this case, I’m imagining I have a count of items somewhere on the page, and so if that array changes, I’m updating the count.

So if you’re passing 2 or 3 collections into your component from createContainer, and only one changes, componentWillReceiveProps will always be executed no matter what. It’s up to you to figure out what changed and how to handle that.

1 Like

Yeah I know this part. But If you add a new item in your example, will React “think” that the prop orders has also changed (because it should get a new reference if Tracker reruns after you’ve inserted a new item)?

No, React won’t think that orders has changed. It actually has no idea which prop changed. If you are passing 8 props into a component, and only one of those changes, componentWillReceiveProps will be called. React just knows that one or more props changed. Like I said, it’s up to you to determine what changed, if it’s important to keep track of course, and act upon it.

2 Likes

You don’t need to use Tracker.autorun in composeWithTracker (and you shouldn’t).
It already runs in a Tracker.autorun.

If you use multiple reactive-data-sources in one composeWithTracker, it behaves the same as composeWithTracker.

However, you can compose a new component with multiple composeWithTrackers:


const MyContainer = composeAll(
  composeWithTracker(
    ({ playlistId }, onData) => {
        const songs = Songs.find({ playlistId }).fetch();
        onData(null, { songs });
    }
  ),
  composeWithTracker(
    ({ playlistId }, onData) => {
        const videos = Videos.find({ playlistId }).fetch();
        onData(null, { videos });
    }
  ),
)(WrappedComponent)

So WrappedComponent will receive the props { playlistId, videos, songs })

The second composer will only run, when a video changes. However, the first composer will run when a song changes and if a video changes, because the first composer will get the result of the second composer as props. This is in this case not desirec, but sometimes this pattern is useful, e.g. when the one composer needs the result of another one (e.g. first fetches comments and the second the usernames of these comments)