[SOLVED] How to make createContainer reliably re-render data changes in normalized data


#1

Hey guys I’m going a little crazy with createContainer and I’m hoping you guys can shed some light on this for me.

I’m using

  • React
  • FlowRouter + React Mounter
  • Stateful react component classes

The title includes the phrase “normalized data” but don’t worry, I’m not referring to SQL style data, just something much simpler, I only need data from 2 collections.

Typical example

export class Foo extends Component {
 constructor (props) {
  super(props);
 }
 render() {
  const {a,b,c} = this.props;
  return <div>{stuff}</div>
 }
}

export const FooContainer = createContainer((props) => {
 //blah
 return {
  some,
  objects,
  based,
  on,
  hopefully,
  reactive,
  datasources
  };
}, Foo);

This sort of scenario works fine for dead simple pages. But whenever I’ve got more complex stuff going on I struggle with absolute pain trying to get the createContainer magic to react to my data sources.

So here’s an example.
Lets say I’ve got a 2 collections:

  • cars
  • drivers
    car documents contain an driverId (the owner of the car)
    So on a page about a specific car, I want to also display stuff like the drivers name and phone number.

This requires

  • I subscribe a cars publication, passing the carId I’m interested in
  • I wait for the cars subscription to be ready, get the driverId from the car document, then subscribe to a drivers publication with the driver’s id as a parameter.

Sounds simple enough. But this is an admin page and I need to be able to change the driverId (owner) of the car. When that happens, I need my page to react to that, and load the new driverId’s data.
This sounds really simple but it’s been an absolute pain to get working.

So just based on this, description, if you know your way around createContainer and can easily achieve this functionality how would you suggest to structure the code?


What I’ve done
It’s a little late now so I can’t go into too much detail about what I’ve tried etc.
But basically I’ve discovered:

  • Any values that might change, (such as URL query parameters [when clicking on an anchor tag (link) to the same page, but a different GET query]) must not be defined in the constructor. Likewise subscription code needs to go somewhere that it can re-run.

  • Putting subscription code in functions and then calling them from the createContainer function as well as any event handlers that might require different data (for example setting a different driverId on a car document (via Meteor.call) seems to allow me to change the subscription reliably, but then I don’t necessarily get a re-render, even when I need it. Then I try hacks of intertwining things with setState to try force a re-render. It’s messy and pulling on one part often causes unwanted adverse side effects.

  • Putting subscription code inside the createContainer function doesn’t give me the reactivity that I need. I need to subscribe to a specific carId that comes from a query param, then onReady, I need to subscribe to the driver of that car. Then when the driver of the car is changed, I need it to react and re-run my driver subscription, and re-render my page.

This is what I’ve been struggling with. Meteor documentation doesn’t explain how the createContainer magic works. The documentation is extremely minimal, so minimal infact that I couldn’t see any real explanation of how it actually works and what it actually responds to.

I’ve tried stuff like making sure I refer to the relevant collections (cars and drivers) inside the createContainer function, and pass objects from .fetch() queies to them as return values. It’s pretty dodgy though as some of the time one or more subscriptions aren’t ready. It would be so much better to have a callback instead of “returning”.
I’ve also tried arbitrarily inserting all kinds of data from cars and drivers that came from createContainer into my rendered output to try stimulate or force re-renders etc, but not getting any solid results.

Just as a little disclaimer, my oplog tailing is definitely working, if I modify my db in mongo terminal for the stuff it wants to react to, it reacts immediately.

I’d like a little less magic and a little more control, if possible.
I’d really like to be able to stack one callback inside another like a normal node.js app so that I can get all my subscriptions ready and then render. And somehow detect when certain changes have happened and perform a re-render.

I’m a little lost and would really appreciate if someone can tell me the Meteor way.
I see Meteor trying to be simple with it’s fibers but it seems to over-simplify.

Another example is if I do a Meteor.call and inside the server method I need to run an asynchronous function, there’s no way to setup a callback function in that method to callback the client who did the Meteor.call.
I get that Meteor wants to be simple for non-asynchronously minded people, but it should always provide asynchronous interfaces as well in my opinion.

But as I said I really do have an open mind and look forward to someone telling me I’m doing it wrong, seeing it the wrong way, and there’s a simple “Meteor solution”

Subscribe inside render()
Lastly, another thing that I tried on a different page with success, but not the current page:

  1. I returned collection results from my createContainer function, but without any subscription data.
  2. Then inside the render I performed a subscription based on this.state.
  3. That allowed me to setState from event handlers.
    Result: It gave me all the reactivity I needed (on the page that worked, the last one I tried it on, infinite render loop, which I couldn’t explain or solve).

Please help!

Thanks

Much appreciated


#2

What I’ve not considered yet, and am considering now is Tracker.

However this post seems to suggest that anything that can be done with Tracker, can be done with createContainer :confused:


#3

Maybe my entire problem comes from using a callback on the first subscribe, inside createContainer. because that callback doesn’t run while createContainer runs, so it probably doesn’t “attach” to the reactive data sources referred to in that function. So maybe I need to make all of my code inside createContainer synchronous.


#4

Yes, as per my last post, it seems that was the entire cause of the problem.


#5

I have only one subscription which returns two cursors from two different collections and I have the same problem.

Example:

export default createContainer(() => {
  Meteor.subscribe('tasks');

  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
    users: Meteor.users.find().fetch(),
    currentUser: Meteor.user(),
  };
}, App);

And the publication:

Meteor.publish('tasks', function () {
  const tasks = Tasks.find({
    $or: [
      { private: { $ne: true } },
      { owner: this.userId },
    ],
  });

  const userIds = _.pluck(tasks.fetch(), 'owner');

  return [
    tasks,
    Meteor.users.find({
      _id: {
        $in: userIds
      }
    }, {
      fields: {
        username: 1
      }
    }),
  ];
});

When I insert a task, all the tasks are displayed on all the clients, but I don’t get the users data. Do you know why?

Thank you in advance,

Samy


#6

It turned out to be because the publications are not reactive.
You can find the solution here: https://www.discovermeteor.com/blog/reactive-joins-in-meteor#the-problem