I have a crazy idea for an improvement for useTracker. If you followed the implementation of the hook, you may have noticed it wasn’t exactly straight forward to get all the parts working together, because of the way that React manages hooks. We ended up relying on a number of difficult to choreograph inline invocations and timeouts to make sure we aren’t running in to trouble with concurrency mode, adjacent suspense, or error boundaries.
First, what’s the problem? The problem is that Meteor’s Tracker sets up a computation - this pattern sets up observers to “reactive” data sources for you, so you don’t have to wire that up and manage it all, and remember to remove event listeners later, etc… SUPER convenient. But that computation is a “side effect” - which we cannot set up in a stateless functional component in the render body of the function, without getting deopted, and losing the benefit of concurrency mode (and risking memory and resource leaks). It has to run later in useEffect
or similar to make sure it’s not getting instantiated 4 times, and never cleaned up. (We work around this by running it in render, but then carefully cleaning up multiple possible instances with timers, and resuming the one that gets mounted if we can - it’s complicated). This is true for every kind of tracker operation, not just computations which contain a subscription, because they all set up observers.
Our hook works well! But a pattern is emerging to be able to leverage React’s powerful feature - Suspense - for async data fetching that would make the implementation of our hook much cleaner, and allow us to better leverage concurrency mode. However there is a cost. Here’s what that pattern - Render-as-You-Fetch (using Suspense) - looks like in pseudo JSX:
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
(ProfileDetails would have either useTracker
or withTracker
integrated.)
So adding Suspense isn’t so bad - we might want that anyway, in cases where there’s a subscription! It’s a little weird though in cases where we don’t have a subscription. And it wouldn’t display for very long, probably under a second in most cases in concurrency mode.
But there’s actually another complication. In the React doc linked above, they suggest triggering “loading” in an event handler. I see two problems with that - one with the suggestion in general, and one specific to Meteor. In general, starting the loading in a click handler (or similar) is fine, except in cases where you might want to trigger load from a route change (which for me is almost all the time). In that case, you’d want something in your component hierarchy to do the loading. For Meteor it’s more of a problem than that though, because we don’t normally consider the computation separate from our specific useTracker
instance.
(We could consider a “resource” API as suggested in the React doc, which decouples the creation of the computation from the “read” operation from the computation. I’m going to think on this more, but we may still want something like I’m about to describe anyway, for managing cleanup - which the React article does not describe.)
So, one solution to this, is we could hoist the creation of the computation up to a Context Provider. The Provider would be responsible for managing the computation, the useTracker
hook would check status of that provider though the context API, and if it’s not ready, would throw to the Suspense handler. That brings our minimum pseudo JSX to:
<TrackerProvider>
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
</Suspense>
</TrackerProvider>
And of course, we could add an Error Boundary in there too. Anyway, there are a few questions in here. Which component should contain the actual tracker function? Should that be defined in the ProfileDetails component (that’s how it’d be now), or should it just be defined in the TrackerProvider, and a hook used in ProfileData to “read” the state? It could still be in ProfileData, and we just pass that up to TrackerProvider - and that adds some other questions, like can 1 provider manage multiple Tracker children? If we might have more than one child which needs a Provider, how do we keep track of which is which (might require an id spec or something in the hook)?
Additionally, maybe we can make this even easier, and combine the TrackerProvider and Suspense in to a custom combo component. Again, it’s a little bit weird, intuitively, that we even need a Provider, for cases when we don’t actually have a long delay or a subscription. There are things with short initialization timings that are tricky - like, you probably want a delayed “fallback” display on your Suspense component from displaying, to avoid ugly flashes of Loading. A Meteor version of Suspense could probably help manage that.
I didn’t mention it above, but this is more efficient than the pretty good workarounds we have now in a very specific way. If you have an expensive Mongo query in your tracker function, that’s currently being run in-line with your render function, which can slow things down. Concurrency Mode will help disguise that, but it will run multiple times (that’s what concurrency mode does)! With a Suspense model like I’ve described above, the query would only run once when the component render is “committed” (when it’s mounted). It’s very efficient.
Really, the question is simply, is this too much API to require?
Ultimately, I’d like to create a way to make this work which leaves the current model in place when no Provider or Suspense is used, but use the more efficient Provider/Suspense model where its available.
Another option would be to try to implement this for the more specialized hooks I mentioned elsewhere - but there’s really no reason this can’t be done for useTracker
.