Meteor React Native in production

I am trying to build a first app based on Meteor React Native and stumbled upon quite some issues, maybe just because I am super new to both React Native and Meteor RN.

I also learned that the original maintainer has moved on from React Native in the meantime, which can of course happen if priorities change.

So I am wondering if anyone is using this package (or React Native in combination with Meteor in general) in a production app?

Hi Tom! I’ve built a couple of them. The learning curve is pretty steep. There was a good tutorial on dev.to but it may be dated by now.

A few years ago, I setup a starter meteor mobile repo based on RN. Would it be helpful if I open sourced that?

Hi Allen, Thanks for the feedback. Yes, that would be cool. I already got it up and running, but I am still struggling with some basic stuff, like reactivity on the user object. The whole app crashes and resets the login state if I try this. Maybe I’m just too dumb.

I just want to make sure that package works before I bet on it. I already built my own DDP stuff twice (once for Unity, once for Swift), and I would love to re-use an existing one before I get the third one rolling. It was always tough to get things right.

Fresh eyes on Meteor React Native are really valuable: that’s how we find out what is unclear, badly documented or simply broken. The issues you opened (#176, #177, #179) are exactly the kind of feedback that helps. So please keep sharing your questions, doubts and critiques, here or on GitHub. It moves things forward for everyone :+1:

@jkuester @fidelsam1992
These are my go-to people when asking anything about Meteor + RN

EDIT: @fidelsam1992 was my boss at some point and he’s the CTO of a Determinds, they’ve ran Meteor + RN in production for ages and have plenty of experience to share.

I’m maintaining my react native app but I’m using Apollo/GraphQL instead.

Thanks for your kind words. I was already in doubt if I might be bit harsh. But happy to support the project. At the moment, I don’t feel strong enough in RN to actually contribute, but that may change in the future.

After playing around with the package, I’m actually a bit confused how reactivity works here. I tried to use `useTracker()`, but I cannot get any reactivity with it. I have been calling Meteor.userId() and find() on a Mongo collection (the ‘users’ collection), but none of these calls seems to trigger re-renders. Is there any demo app available to see the Meteor RN package in action?

Ok, nevermind. Got the reactivity working.

We use @meteorrn/core (2.9.1) in two clients for the same Meteor backend: An iOS app on Expo 54 / RN 0.81, and an Electron desktop app that runs the same package in the renderer. The iOS app is still in TestFlight. I’ve got about 15 testers. It covers subscriptions, methods, accounts and resume tokens without trouble.

It has some rough edges, but you can work around all of them from your own app. The ones I hit:

  • Types (#176): we declare our own interface for the Meteor methods and cast the default export to it.
  • Browser (#177): Before importing the package, I polyfill global, setImmediate and process.nextTick. It works in Electron with that.
  • Login state on cold start: Meteor.user() is empty until the resume login finishes after connect, so don’t treat !user as “signed out”. Meteor.loggingIn() is true for the whole window; show a spinner during, and only then route on the user. (If you want to avoid a spinner whe there’s no stored token, check AsyncStorage for reactnativemeteor_usertoken first.)
  • useTracker(#179): the package’s hook creates its computation during render, which causes the error. I swapped in a react-meteor-data-style hook that computes the first value with Tracker.nonreactive and runs the autorun inside a useEffect. Reactivity on Meteor.user() works well after that.
  • One thing to look out for: A doc that leaves a filtered find() (like, its status changes) sometimes doesn’t trigger a re-run. Having my hook also re-run on Meteor.getData().onChange fixed it.
  • Dupe subs: Each component that calls Meteor.subscribe gets its own server-side sub, so I hold shared ones in a provider at the root.

Thanks for the detailed feedback!

we declare our own interface for the Meteor methods and cast the default export to it.

Yep, that’s how I solved it now. The existing types were either buggy or non-existant (e.g., for Mongo).

I already polyfilled setImmediate(), that seemed to be enough for the browser. But thanks for letting me know that there might be two other candidates

Awesome. Mind if you share this hook here?

I hope this scales well. Wasn’t aware of this. I guess they are not re-using the subscription ids properly? I faced similar issues in another DDP implementation I adapted (for Swift). It’s actually not that easy to get DDP right, even if the protocol itself seems simple at first.

No problem. This is from my Electron client (the mobile one does the same, but clumsier). The mobile app came first and used the package’s userTracker as-is. Expo doesn’t turn on StrictMode, so the hook’s main flaw never surfaced there, and it worked well enough that I had no reason to touch it. The Electron app came later with StrictMode on, components stopped updating, and I had to rewrite it. That’s how the Electron app got the cleaner version.

I’d still recommend my Electron’s version to anyone starting fresh, which is why I’m posting it. The package’s own hook has two flaws that my Electron app avoids. It sets state during render, which is the #179 warning. And under StrictMode its components stop updating. I just haven’t hit either on mobile so I haven’t got around to updating it.

import { useEffect, useRef, useState, type DependencyList } from 'react';
import Meteor, { Tracker } from '@meteorrn/core';

type MeteorData = { onChange(cb: () => void): void; offChange(cb: () => void): void };
const meteorData = () => (Meteor as unknown as { getData(): MeteorData }).getData();

export function useTracker<T>(reactiveFn: () => T, deps: DependencyList = []): T {
  // First value computed during render, but without creating a computation.
  const [value, setValue] = useState<T>(() => Tracker.nonreactive(reactiveFn));

  // Always run the latest closure without restarting the computation every render.
  const fnRef = useRef(reactiveFn);
  useEffect(() => { fnRef.current = reactiveFn; });

  useEffect(() => {
    const computation = Tracker.autorun(() => setValue(fnRef.current()));

    // Also re-run on any minimongo write: the package can miss a doc *leaving*
    // a filtered find(). offChange needs the same function reference back.
    const data = meteorData();
    const onChange = () => computation.invalidate();
    data.onChange(onChange);

    return () => {
      data.offChange(onChange);
      computation.stop();
    };
  }, deps);

  return value;
}

Use it like the package’s: const user = userTracker(() => Meteor.user()); The computation only lives in the useEffect, so render never sets state, (#179), and StrictMode’s unmount/remount leaves one live computation. The onChange part is the safety net I mentioned. It costs a local query and a re-render per batch of writes. Tracker merges invalidations, so it’s cheap.

Correction on the subscriptions I mentioned. It scales fine and the IDs are fine, and it’s not a meteorrn bug. Its own subscribe() is a straight port of Meteor core’s, and real Meteor behaves the same way. A sub is reused only when the same computation re-runs with the same name and args, so two components subbing to the same thing open two subs on the web as well. The merge box sends each document once regardless, so the cost is extra publish runs on the server, not extra data. I consolidated mine because React Navigation keeps the screens underneath mounted, and I ended up with six copies of one publication, which also meant six places to get reconnect handling right. A provider at the root that subs once and passes ready() down through context fixed both.

Thank you so much for sharing this!

A follow up now that you have things running. There are a few more things worth knowing about:

  • Every reconnect empties every collection. The connected handler runs remove({}) on all collections as soon as the socket reopens, before the resume login and before your subscriptions have a re-run. Real Meteor keeps the old docs and reconciles once the subs are ready. So on every reconnect, your trackers briefly read nothing. On a phone every return from the background can be a reconnect, so lists blink empty and ‘document is gone’ logic can fire. Subs are also marked not ready on disconnect, which at least lets you tell them apart. Either hold the last good value across the gap, or snapshot the collections on disconnect, restore them on connect, and remove what the server didn’t re-send once everything is ready. That’s issue #175.
  • Method calls can be lost, and they never timeout. Three ways: 1. A reply lost to a reconnect: The await hangs forever. Only a result clears a pending call. 2. A call made while offline: It waits in a queue that’s emptied whenever the socket closes, including failed reconnect attempts. 3. A call that does go out on reconnect: It’s sent before the resume login, so it runs on the server as a logged-out user. The package also answers the server’s pings but never notices when they stop. I now wrap Meteor.call in a promise function that waits for the socket and the login, times out, re-sends only calls that are safe to run twice, and fails the rest with a clear error, plus a watchdog that closes a socket that has gone quiet. Our version is recent, so I can’t vouch for it in the field yet, but the problems are easy to confirm in the package’s code.
  • Subs restart before the login has finished. After a reconnect they go out right behind the login message, so a pub that depends on this.userId answers twice: empty first, then the real data. Don’t take the first ready after a reconnect as final for user-specific data.
  • A field the server $unsets stays on the client as null instead of being deleted. This affects $exists: false queries. Read fields with != null or ??.
  • With large collections: Every incoming doc change is matched against every live cursor with a full scan of the collection (no _id shortcut). That’s from reading the code; I haven’t hit it at our size.

Cheers

Thanks for the additional info. Emptying the entire collection on every reconnect is not what I would expect for a mobile app. Same goes for the missing timeout in the method calls. I guess the package needs some fine-tuning. I was facing similar issues when using existing DDP packages for C# and Swift. Had to patch them a lot. It’s quite tough to rebuild the logic Meteor’s web implementation uses. The DDP protocol itself is pretty lightweight, but the devil is in these details… I wish we could just use `react-meteor-data` for React Native as well. Or a plugin for react-meteor-data that enables a different storage backend for local data. Like SQLite or - eben better - LiveStore.