[Solved] Meteor 3: Subscription.ready() not reactive anymore?

I have a Tracker.autorun() in my client code that automatically subscribes to my user’s notifications, like this:

  Tracker.autorun(async function() {
    const subscription = Meteor.subscribe('notifications:notifications');
    if (!subscription.ready()) return;
    ... fetch the notifications ...
});

This worked in Meteor 2. But in Meteor 3, the autorun() won’t be triggered if the ready() state changes. I assume that ready() is not reactive anymore? Is there some documentation about this change, and how can such a reactive listener be achieved in Meteor 3 now?

I also noticed that .ready() never returns true. If I remove the if line, at least I get the data, yet it will run unneccessary often now.

1 Like

I moved away from reactive subs within Tracker computations because they interfered too much and I used the callbacks instead (onReady and onError) which are pretty stable. Using them in combination with reactive-var/-dict works very well in Blaze.

1 Like

Hey! Which version are you in?

The code for the ready is here.

One thing that can happen is your sub is not ready for some reason, and you can check if it is there

Meteor.default_connection._subscriptions can show all subscriptions you have

You can check that one’s ID using subscription.subscriptionId.

Also, I saw that you are in an async context. Are you wrapping the reactive await calls with Tracker.withComputation?

Thanks, that actually worked!

Hi @grubba,

Thanks for the fast response. I am on Meteor 3.0.4. The sub actually becomes ready, as I can check with @jkuester’s approach. I found the code for ready() in the meantime and saw it stil had a depend call, but still my autorun() won’t re-run if the sub gets ready.

Nope, I do not wrap then. I wasn’t aware this is neccessary and did not see it in the Meteor 3 upgrade docs. This could explain why the ready() call never triggers, because I have an await Meteor.userAsync() before that call.

I’m a bit confused with the docs. Is this pseudo-code (i.e. Computation is a type)?

import { Tracker } from "meteor/tracker";

Tracker.withComputation(
  Computation,
  () => {},
);

Also, the line

let links = await LinksCollection.findAsync({}).fetch(); // First async call will stay reactive.

seems wrong to me, since find() is not async, but fetchAsync() would be.

1 Like

Yes!

It is a function from "meteor/tracker" that requires a Computation type object as first param and has a function as it second param.

Yes lol! I just checked, and I have mistakenly transcribed this part in the past :man_facepalming:

This was one of the sources:


// needs Tracker.withComputation because otherwise it would be only called once, and the computation would never run again
Tracker.autorun(async (c) => {
    const placeholders = await fetch('https://jsonplaceholder.typicode.com/todos').then(x => x.json());
    console.log(placeholders);
    const data = await Tracker.withComputation(c, () => LinksCollection.find().fetchAsync());
    console.log(data);
});

I’ll write a PR updating those mistakes!

but basically, a rule of thumb is that if you are using a reactive function for example find + fetchAsync (Meteor.userAsync() uses it under the hood), it is nice to wrap it inside Tracker.withComputation

I tried the withComputation() approach now, but this got me into an endless loop.

Here’s the full code example:

Meteor.startup(function() {
  let native = unity.active ? unity : visionPro;
  Tracker.autorun(async function(computation) {
    const user = await Tracker.withComputation(computation, async () => await Meteor.userAsync());
    const { notifications: userNotifications = {} } = user || {};
    let { since } = userNotifications;
    if (user && !since) since = new Date(0);
    const subscription = Meteor.subscribe('notifications:notifications');
    if (!Tracker.withComputation(computation, () => subscription.ready())) return;
    const active = await Tracker.withComputation(computation, async () => await notificationsCollection.find({}, { sort: { createdAt: -1 } }).fetchAsync());
    const unread = await Tracker.withComputation(computation, async () => await notificationsCollection
      .find({ createdAt: { $gt: since } }, { sort: { createdAt: -1 } })
      .fetchAsync());
    store.dispatch(updateNotifications({ active, since, unread }));
  });
});

If I remove Tracker.withComputation on this line:

if (!Tracker.withComputation(computation, () => subscription.ready())) return;

it won’t loop endlessly, but it won’t go past that line either, because ready() never becomes true.

1 Like
Tracker.autorun(async function (computation) {
  const subscription = Meteor.subscribe("notifications:notifications");
  if (!subscription.ready()) return;
  const user = await Tracker.withComputation(computation, () =>
    Meteor.userAsync()
  );
  const { notifications: userNotifications = {} } = user || {};
  let { since } = userNotifications;
  if (user && !since) since = new Date(0);
  const active = await Tracker.withComputation(
    computation,
    async () =>
      await notificationsCollection
        .find({}, { sort: { createdAt: -1 } })
        .fetchAsync()
  );
  const unread = await Tracker.withComputation(
    computation,
    async () =>
      await notificationsCollection
        .find({ createdAt: { $gt: since } }, { sort: { createdAt: -1 } })
        .fetchAsync()
  );
  store.dispatch(updateNotifications({ active, since, unread }));
});

I’ve reworked it a bit, maybe it works…

1 Like

Thanks. This seems to work, without the endless loop.

I am wondering: I always thought that the first run of a reactive function would determine which code be used to trigger re-runs. That was the main reason why I had placed the userAsync() call before the ready() call. But I still get re-runs now when the user object changes (what I want), although the ready() would return on the first call. So I guess that every run will actually add reactive triggers if the code is at least touched once?

1 Like

I also stumbled upon this documentation now:

which also confuses me a bit. I wasn’t aware that useTracker() also needs Tracker.withComputation. And the sample does not really explain why exactly the first version needs withComputation, but the second won’t. Is it becaue it is the only async call and thus the first one?

1 Like

The second one does not need to because we add a withComputation on the useTracker

All is wrapped in one but the async makes the reactivity move around, so we need to inject the reactivity back into the reactive function/source

1 Like

Ok, thanks for the clarification.