1# Mobile development lifecycle
2
3The [connection runtime's HMR boundary](../../apps/mobile/src/lib/hot-swappable-atom-runtime.ts)
4keeps a stable atom runtime and replaces its Effect layer through a writable atom.
5It accepts the update only after installing the new layer. Otherwise importers
6retain the old behavior even though Metro reports a successful refresh.
7
8Do not reset the shared atom registry to refresh a connection-runtime edit. Reset
9removes listeners from mounted consumers that Metro has no reason to rerender.
10When the registry or managed-runtime module itself changes, normal Metro propagation
11disposes its old resources. This boundary does not make arbitrary module-level atom
12families safe to hot-swap. Production uses an ordinary atom runtime.
13
14[Environment supervisor scopes](../../packages/client-runtime/src/connection/registry.ts)
15are children of the registry scope. The per-environment map supports targeted
16shutdown, but a supervisor created after its cleanup runs would escape it. A closed
17parent scope also closes late arrivals, preventing interrupted startup or runtime
18replacement from leaving a WebSocket alive outside the new registry.
19
20Uniwind compiles CSS on Metro updates so newly used classes are discovered. It
21skips global style invalidation only when the generated stylesheet and theme list
22are unchanged. Skipping compilation would lose new classes; invalidating every
23consumer for unchanged output makes an ordinary component edit refresh the whole
24app. The fingerprint is recorded only after initialization succeeds.
25
26The native modules under `apps/mobile/modules/` are `file:` dependencies, and pnpm
27copies those into its virtual store instead of linking them. Metro bundles the copy,
28so an edit to a module's TypeScript is invisible to a running dev client until
29`vp i` re-syncs it, while Gradle and CocoaPods compile the worktree directory
30directly. A JavaScript change that "has no effect" on device is usually this.
31
32## Measuring renders
33
34[`render-counter.ts`](../../apps/mobile/src/features/development/render-counter.ts) counts the
35components React rendered in each commit and why: a changed state slot, changed props, or
36context. It wraps the DevTools hook from the first import in `index.ts` and loads only when
37`__DEV__` and `EXPO_PUBLIC_T3_RENDER_COUNTER=1`, so release bundles never contain it. The walk
38skips subtrees React bailed out of. A probe that checks `flags & 1` on every fiber reports
39renders that never happened, because bailed-out fibers keep flags from older commits.
40
41[`measure-renders.mts`](../../apps/mobile/scripts/measure-renders.mts) drives a measurement from
42outside the app. It evaluates the counter's globals through Metro's Hermes inspector proxy, which
43works the same for iOS simulators and Android emulators and needs no taps, `simctl`, or `adb`. It
44starts a real provider turn through the server, opens the thread with an app link, records a window
45mid-stream, and prints the median renders, commits, top components and top causes. Expo drops inspector sockets whose `Origin` is not the URL Metro advertises, so
46pass that URL (the LAN address with `--lan`) as `--metro`.
47
48```bash
49EXPO_PUBLIC_T3_RENDER_COUNTER=1 vp run dev:client # in apps/mobile, then pair the app
50node apps/server/src/bin.ts auth session issue --base-dir <base-dir> --token-only
51node apps/mobile/scripts/measure-renders.mts --server <server-origin> --token <token> \
52 --metro http://<metro-lan-host>:8081 --project-root <scratch-dir>
53```
54
55`--route /settings/usage` instead reloads the app, opens that path from Home and records a 6 s window over
56mount and data settling, with no provider turn and no `--server`, `--token` or `--project-root`.
57With `--server` as well, the app reloads, opens that path (`/` stays on Home) and the reply streams in a
58new thread the app never shows, which measures what streaming elsewhere costs the screen in front.
59
60Compare before and after on the same base, on the same device, with the median of three runs.
61Provider output varies run to run, so a single run or a comparison across bases is noise.
62
63## React Compiler
64
65Metro runs the compiler only because `experiments.reactCompiler` is set in `app.config.ts`. Expo
66then passes `supportsReactCompiler` to `babel-preset-expo`, which owns the plugin options; there is
67no `babel.config.js` entry to look for. Release bundles use `panicThreshold: "NONE"`, so a function
68the compiler can't prove safe ships unmemoized with no build error.
69[`reactCompilerCoverage.test.ts`](../../apps/mobile/src/reactCompilerCoverage.test.ts) runs the
70same preset over every source file. It fails on a new bailout, and on a file in `KNOWN_BAILOUTS`
71that now compiles, so the list only shrinks. When you fix a listed file, delete its entry and its
72manual `useMemo`/`useCallback`/`memo` in the same change. Most mobile bailouts are `try` without
73`catch` or with `finally`, and refs read during render; see [Web UI](web-ui.md#react-compiler)
74for the usual rewrites. Mobile runs `t3code/no-manual-memo` with `allowMemoHooks`, so memo
75directives are errors but the hooks stay legal until `KNOWN_BAILOUTS` is empty.
76`t3code/no-hookless-use-function` rejects a `use*` function that calls no hook, since the compiler
77leaves it uncached. A platform stub of a real hook, such as an `.android.ts` file, keeps the hook
78name and suppresses the rule with a reason.