Meteor 3.0 - issues with suspend useTracker

We have several fairly large Meteor apps running on 2.13 and are in the process of migrating to Meteor 3.
All of our apps have a shared core with a lot of isomorphic code, which we need to make async for Meteor 3. A lot of this isomorphic code is used in useTracker on the client-side.

The official V3 migration guide for React instructs to use the suspense version of useTracker, which can handle async functions, but we found several issues blocking us and we are not sure how to proceed:

  • If we put a Suspense high in the component tree it results in a buggy mess, sometimes react errors about bad setStates are thrown and the app never loads
  • Putting the Suspense just above the trackers makes things better at first glance, but we start running into the issues below
  • Using between 1 and 4 suspense trackers in one component results in flaky UI flashes (sometimes no flashes, sometimes several)
  • Using more than 5 suspense trackers in one component starts making things unusable, where each one makes it exponentially worse. UI flashes several times and each flash takes several seconds, resulting in atrocious loading times. Using Tracker.withComputation inside the suspense tracker seems to only make things worse.
  • FindOneAsync does not seem to keep its reactivity, even with Tracker.withComputation. Changing to FindOne instantly brings the reactivity back. We found that using a workaround of .find.fetchAsync()[0] still keeps things reactive.

Keep in mind all we are doing is replacing the normal trackers with the suspense ones (collection data is received from global subscriptions). It doesn’t matter if the tracker returns a promise or not. The problem can easily be reproduced like this:

  const a = useTracker('a', () => Collection1.find().fetchAsync());
  const b = useTracker('b', () => Collection2.find().fetchAsync());
  const c = useTracker('c', () => Collection3.find().fetchAsync());
  const d = useTracker('d', () => Collection4.find().fetchAsync());
  const f = useTracker('e', () => Collection5.find().fetchAsync());
  const e = useTracker('f', () => Collection6.find().fetchAsync());

This is the most basic of examples, with which we are running into problems. Ideally we would use our shared isomorphic code in the trackers, which can call several async methods, have additional logic, etc.

We are running into this problem on both Meteor 2.13 and 3.3.1. Versions of related packages:
react-meteor-data@4.0.0
react: 18.2.0

Are we missing/misunderstanding/misusing something? To us it looks like the suspense trackers are an experimental feature, yet they are a part of the official migration guide.

Please run this question in ChatGPT to understand the other option: “I have a project built with Meteor 3 and React 18. Show me a good example for the use of Suspense and useTracker, considering that I have many functional components that use this pattern.
Instead of suspense, can I just early return a span conditioned by receiving the data?”

A problem you might encounter, observed as those flickers, may come from waterfall rendering - the child of a child of a child of a parent.
All intermediate children (and parent) with props and state updates must make use of useMemo to condition the rendering of its children.

One other way to design your React frontend is to break intermediary children into fixed components and data-dynamic components and do all data processing in the subcomponent.
For example if you had

const data  = ... // result of fetching from Meteor.

// component A....
<div>
  <h1>
  <p>
// ....
<ul>
 {data.map(d => map to a list of li)
</ul>
</div>

Move the list into a new component, component B so that component A is not rerendered due to changes in data.
I have this example from ChatGPT:

const { tasks, isLoading } = useTracker(() => {
  const handle = Meteor.subscribe('tasks');
  const loading = !handle.ready();
  const data = TasksCollection.find({}, { sort: { createdAt: -1 } }).fetch();
  return { tasks: data, isLoading: loading };
});

What happens:

  1. Meteor sets up a Tracker computation internally.
  2. Inside that computation, anything reactive (like a publication handle, a Mongo query, or a ReactiveVar) becomes a reactive dependency.
  3. Whenever one of those dependencies changes (for example, the TasksCollection changes because of an insert/update/remove in Minimongo), the computation invalidates.
  4. When it invalidates, useTracker runs your function again and causes a React state update internally — which triggers a re-render of the component.

So, even though tasks is just a returned value, React receives new data via this state update every time the Tracker reruns.

:small_orange_diamond: In short

:white_check_mark: Yes — when TasksCollection changes (reactively), your component will re-render.

No need for manual state or prop wiring. useTracker handles that.


:small_orange_diamond: Bonus: what doesn’t trigger re-render

  • Non-reactive data (like a plain Meteor.call or local variable).
  • Non-tracked reactive sources outside the function passed to useTracker.
  • Data changes that don’t affect the fields you fetch (e.g., you only fetch { text } but update { status } and your query filters by text).