Describe the bug
Summary
On Expo SDK 56, Reactotron's networking timeline shows no network requests. SDK 56 installs expo/fetch as the global fetch by default, and expo/fetch is a native implementation that does not go through XMLHttpRequest. Reactotron's network instrumentation only patches XMLHttpRequest, so it never observes any traffic. Console log/display events are unaffected — only the network timeline is empty.
This affects both project types:
- Managed Expo apps —
expo is loaded at startup (e.g. via expo-router/entry), so the fetch swap happens from app launch and the network timeline is empty from the start.
- Bare React Native apps that use Expo packages — the swap runs the first time anything imports the
expo package at runtime. Importing an Expo package triggers expo/src/Expo.fx, which loads the Winter runtime and replaces global.fetch. For example, calling useCameraPermissions() from expo-camera (which imports from expo) is enough. So in a bare app the network timeline can appear to work and then go empty once such a code path is loaded. (A type-only reference like useRef<CameraView>(null) is elided by the Babel pipeline and does NOT trigger it — only a runtime import does.)
Environment
reactotron-react-native: 5.2.0 (also confirmed against master, last pushed 2026-05-28)
- Reactotron desktop: 3.11.0
- Expo SDK: 56
- React Native: 0.85.3 (New Architecture)
- Platform: iOS & Android
- Reproduced on both a managed Expo app and a bare RN app that uses Expo
packages (hybrid bare workflow)
Root cause
Expo SDK 56 makes expo/fetch the default globalThis.fetch (Expo docs, SDK 56 changelog). It's a fully native fetch that bypasses XMLHttpRequest.
Reactotron's interceptor patches only XHR:
lib/reactotron-react-native/src/xhr-interceptor.ts — patches XMLHttpRequest.prototype.open / send / setRequestHeader
lib/reactotron-react-native/src/plugins/networking.ts — relies solely on XHRInterceptor.enableInterception()
The assumption is even documented in xhr-interceptor.ts:
"This supports interception with XMLHttpRequest API, including Fetch API and any other third party libraries that depend on XMLHttpRequest."
That assumption — that fetch is built on XHR — no longer holds under SDK 56's expo/fetch.
Steps to reproduce
- An SDK 56 app — either a managed Expo app, or a bare RN app that imports an Expo package at runtime (default config, so
expo/fetch is the globalfetch).
- Configure Reactotron with
.useReactNative() (networking enabled) and .connect().
- Make any
fetch(...) request (in a bare app, ensure the expo package has been imported — e.g. useCameraPermissions() from expo-camera).
- Observe the Reactotron timeline.
Expected
The request appears in the networking timeline.
Actual
Network logs are missing in Timeline. App stays connected.
Workaround
Until the fix lands, add this line to your .env file:
EXPO_PUBLIC_USE_RN_FETCH=1
This is Expo's own opt-out — it keeps React Native's XHR-based fetch as the global, which Reactotron's existing networking plugin already instruments, so requests show up in the timeline again.
Trade-off: the app forgoes expo/fetch's improvements (brotli/gzip/zstd decompression, AbortSignal.timeout). Because EXPO_PUBLIC_* vars are inlined into the JS bundle at build time, prefer a dev-only env file (e.g..env.development) so the opt-out doesn't ship to production.
Related / prior art
Sentry hit and fixed this exact problem:
Their approach: detect that the global fetch is the native Expo implementation via the marker globalThis.fetch[Symbol.for('expo.builtin')] === true (a new isExpoFetchEnabled() helper), then enable fetch-layer instrumentation when detected (they flipped traceFetch and fetch-breadcrumb defaults from false to isExpoFetchEnabled()).
Possible fix
The catch for Reactotron: Sentry already had a fetch-instrumentation layer and only needed to change a default. Reactotron is XHR-only (xhr-interceptor.ts / networking.ts patch XMLHttpRequest exclusively), so there's no existing fetch layer to enable — a fetch interceptor would need to be added.
A minimal version: wrap globalThis.fetch when the expo.builtin marker is present and emit reactotron.apiResponse(request, response, duration) — the same event the XHR path produces. Gating on the marker avoids double-counting when EXPO_PUBLIC_USE_RN_FETCH=1 reverts the global to RN's XHR-based fetch.
Reactotron version
5.2.0
Describe the bug
Summary
On Expo SDK 56, Reactotron's networking timeline shows no network requests. SDK 56 installs
expo/fetchas the globalfetchby default, andexpo/fetchis a native implementation that does not go throughXMLHttpRequest. Reactotron's network instrumentation only patchesXMLHttpRequest, so it never observes any traffic. Consolelog/displayevents are unaffected — only the network timeline is empty.This affects both project types:
expois loaded at startup (e.g. viaexpo-router/entry), so thefetchswap happens from app launch and the network timeline is empty from the start.expopackage at runtime. Importing an Expo package triggersexpo/src/Expo.fx, which loads the Winter runtime and replacesglobal.fetch. For example, callinguseCameraPermissions()fromexpo-camera(which imports fromexpo) is enough. So in a bare app the network timeline can appear to work and then go empty once such a code path is loaded. (A type-only reference likeuseRef<CameraView>(null)is elided by the Babel pipeline and does NOT trigger it — only a runtime import does.)Environment
reactotron-react-native: 5.2.0 (also confirmed againstmaster, last pushed 2026-05-28)packages (hybrid bare workflow)
Root cause
Expo SDK 56 makes
expo/fetchthe defaultglobalThis.fetch(Expo docs, SDK 56 changelog). It's a fully native fetch that bypassesXMLHttpRequest.Reactotron's interceptor patches only XHR:
lib/reactotron-react-native/src/xhr-interceptor.ts— patchesXMLHttpRequest.prototype.open/send/setRequestHeaderlib/reactotron-react-native/src/plugins/networking.ts— relies solely onXHRInterceptor.enableInterception()The assumption is even documented in
xhr-interceptor.ts:That assumption — that
fetchis built on XHR — no longer holds under SDK 56'sexpo/fetch.Steps to reproduce
expo/fetchis the globalfetch)..useReactNative()(networking enabled) and.connect().fetch(...)request (in a bare app, ensure theexpopackage has been imported — e.g.useCameraPermissions()fromexpo-camera).Expected
The request appears in the networking timeline.
Actual
Network logs are missing in Timeline. App stays connected.
Workaround
Until the fix lands, add this line to your
.envfile:This is Expo's own opt-out — it keeps React Native's XHR-based
fetchas the global, which Reactotron's existing networking plugin already instruments, so requests show up in the timeline again.Trade-off: the app forgoes
expo/fetch's improvements (brotli/gzip/zstd decompression,AbortSignal.timeout). BecauseEXPO_PUBLIC_*vars are inlined into the JS bundle at build time, prefer a dev-only env file (e.g..env.development) so the opt-out doesn't ship to production.Related / prior art
Sentry hit and fixed this exact problem:
Their approach: detect that the global
fetchis the native Expo implementation via the markerglobalThis.fetch[Symbol.for('expo.builtin')] === true(a newisExpoFetchEnabled()helper), then enable fetch-layer instrumentation when detected (they flippedtraceFetchand fetch-breadcrumb defaults fromfalsetoisExpoFetchEnabled()).Possible fix
The catch for Reactotron: Sentry already had a fetch-instrumentation layer and only needed to change a default. Reactotron is XHR-only (
xhr-interceptor.ts/networking.tspatchXMLHttpRequestexclusively), so there's no existing fetch layer to enable — a fetch interceptor would need to be added.A minimal version: wrap
globalThis.fetchwhen theexpo.builtinmarker is present and emitreactotron.apiResponse(request, response, duration)— the same event the XHR path produces. Gating on the marker avoids double-counting whenEXPO_PUBLIC_USE_RN_FETCH=1reverts the global to RN's XHR-based fetch.Reactotron version
5.2.0