StatsHub Docs
Reports

Nitro Fetch adoption research

Verified API, runtime limits, migration risks, and an adoption boundary for Nitro Fetch across the Expo applications.

Research snapshot: 2026-08-31. The source review uses Nitro Fetch package react-native-nitro-fetch version 1.6.3 at commit 627c77e. The package metadata names react-native-nitro-modules >=0.36.1 as a required peer and react-native-worklets >=0.8.0 as optional (package metadata, peer dependencies).

Recommendation

Adopt Nitro Fetch as the native transport behind one small HTTP adapter shared by the Expo applications. Do not make an unconditional, cross-platform globalThis.fetch replacement.

Implementation update: StatsHub installs a native-only global replacement in the app entry before Expo Router loads any data clients. The Platform.OS !== "web" guard preserves the recommendation above: web keeps its original global fetch, while native SDK-owned HTTP joins application-owned requests on NitroFetch.

The explicit adapter is the safer boundary because:

  • the package's web entry delegates back to globalThis.fetch; replacing that global with the same exported function would recurse on web (web implementation);
  • the native package creates JSI-backed singleton objects at module evaluation, so Node test environments need a mock or a platform-specific adapter (singleton creation);
  • Nitro's Request and Response are not complete browser implementations, despite the official docs calling the response spec-compliant. Both formData() methods throw, and streamed-response cloning and JSON parsing do not preserve the stream (Request body readers, Response body readers).

Use platform files for the transport:

// transport.native.ts
export { fetch as transportFetch } from "react-native-nitro-fetch";

// transport.web.ts and transport.test.ts
export const transportFetch = globalThis.fetch.bind(globalThis);

Build base URL handling, query serialization, auth headers, HTTP error mapping, and timeout policy in the shared adapter. Nitro Fetch deliberately exposes a Fetch-like primitive; it does not supply those higher-level client features.

Runtime and platform support

RuntimeVerified behaviorMigration consequence
React NativeThe README requires React Native 0.75 or newer and installs react-native-nitro-fetch with react-native-nitro-modules. Native requests use Cronet on Android and URLSession on Apple platforms. Installation and stacksEvery native application needs the native dependencies and a rebuilt development client/binary. A JavaScript-only update cannot add it.
AndroidThe package compiles the app's configured minimum/target SDK values and bundles Cronet. Android buildNo independent package min-SDK number is declared; compatibility follows each app and its React Native version.
iOS and tvOSThe podspec declares both iOS and tvOS using React Native's min_ios_version_supported. PodspeciOS is supported. tvOS is declared but is outside the requested Expo app migration.
WebThe browser export uses the platform fetch. Prefetch, cold-start prefetch, the raw Nitro object, and token-refresh storage only warn or return empty values. Web fallbacksKeep browser fetching on the browser transport. Nitro's native performance and startup-prefetch features do not exist on web.
WorkletsnitroFetchOnWorklet uses optional react-native-worklets; without it, the mapper runs on the JavaScript thread. ImplementationDo not add Worklets merely for the transport migration. Treat off-thread mapping as a later, measured optimization.

Because this is a native JSI module, stock Expo Go cannot contain it. This is an inference from the shipped iOS/Android native code and the required native peer, not an explicit Nitro Fetch statement. Expo applications must use their custom development clients and production builds.

Public API surface

The native entry exports the following symbols (complete export list):

AreaExportsNotes
Fetch primitivesfetch, Headers, Request, ResponseThe intended migration surface.
Prefetchprefetch, prefetchOnAppStart, removeFromAutoPrefetch, removeAllFromAutoprefetchNative-only. __readAutoPrefetchQueue is also exported, but its leading underscores mark it as an internal diagnostic seam; application code should not depend on it.
WorkletsnitroFetchOnWorkletOptional Worklets integration.
Auth for native startup workregisterTokenRefresh, clearTokenRefresh, callRefreshEndpoint, getStoredTokenRefreshConfigThis serves cold-start prefetch/WebSocket prewarm. It is not a general request interceptor.
DiagnosticsNetworkInspector, generateCurl, profileFetchInspector entries can be subscribed to with onEntry; they observe completed activity and cannot rewrite a request. Inspector API
Low-level native objectNitroFetchExposes the JSI object and createClient; use the Fetch-shaped API for application code.

There are no React hooks, request/response interceptors, middleware, retry policy, base-URL client, or query-object API in the public exports. The shared adapter must own those concerns. NetworkInspector.onEntry is an observation callback, not an interceptor.

Request behavior

URL, base URL, and query parameters

fetch accepts RequestInfo | URL. Nitro Fetch does not accept a base URL or a params/searchParams option (fetch signature). Construct the absolute URL and query string before calling the transport. The adapter should use URL and URLSearchParams so every app encodes repeated, empty, and optional parameters the same way.

HTTP and HTTPS use the native network client. data: is decoded in JavaScript; file/content/scheme-less resources go through native local-file handling; and blob: URLs throw with an instruction to use platform fetch or FileReader (local resource routing).

Headers, auth, and cookies

Headers accept Headers, tuple arrays, or plain objects. Normal runtime auth is still an Authorization or application-specific header supplied by the shared adapter. Nitro Fetch has no normal-request token callback.

The credentials values are forwarded, but native behavior is effectively binary: omit disables cookie handling, while same-origin and include both leave it enabled; there is no JavaScript origin comparison in the request builder (request construction, iOS cookie switch). On Android, synchronization with the WebView CookieManager is opt-in and must be enabled in Application.onCreate; iOS URLSession uses its native cookie storage (cookie-sync guide).

registerTokenRefresh stores a mapping configuration for native work that runs before JavaScript. It can map JSON or text responses into headers and, for startup fetches, into JSON or multipart bodies. Its methods are limited to GET, POST, PUT, and PATCH (configuration type). Use it only when an authenticated request genuinely has to start before the JS auth store exists.

Request bodies and uploads

The buffered transport accepts strings, URLSearchParams, FormData, ArrayBuffer, typed-array views, and Blob. Blob input is read into an ArrayBuffer, and its MIME type becomes Content-Type if the caller omitted that header (body normalization, Blob conversion).

React Native file uploads use the usual { uri, type, name } object in FormData; Nitro serializes it to native multipart parts (official upload example). Request ReadableStream bodies are unsupported, and FormData is unavailable in the Worklets request path. Migrate JSON, URL-encoded, binary, and multipart call sites separately so tests exercise each body family.

Abort and timeout

Standard AbortSignal cancellation is supported. A signal that is already aborted rejects with an error named AbortError; an in-flight abort cancels the native request and is normalized to the same name (abort implementation).

There is no public timeout option on the Fetch-shaped API. Although the low-level native request type contains timeoutMs, the JS request builder does not copy such an option into the native request (native type, JS builder result). Implement timeouts with an AbortController timer, as the official guide does (timeout pattern). When TanStack Query supplies a signal, forward it rather than replacing it; a timeout helper must combine cancellation sources or abort the same controller.

Redirects and cache directives

The buffered transport supports redirect: "follow", "manual", and "error". Both non-follow modes stop native redirects; "error" then throws on a 3xx response (redirect handling).

Only cache: "no-store", "no-cache", and "reload" add request headers. "force-cache" and "only-if-cached" are accepted by the type but receive no special JavaScript treatment (cache mapping). Do not treat Fetch cache options as a replacement for TanStack Query's cache.

Response and error behavior

Like standard fetch, HTTP 4xx and 5xx responses resolve normally with response.ok === false; only transport failures, aborts, unsupported inputs, and redirect: "error" reject. Every migrated JSON helper must check ok before parsing or explicitly preserve an endpoint's existing error contract (response construction).

The default path buffers the full native response before resolving. A response.body reader still exists, but it emits that buffered body as one chunk. Progressive server-sent events and token streams require { stream: true }; that separate path does not consult prefetch, always reports redirected === false, and ignores redirect: "error" (streaming contract in source).

Migration tests must cover these source-level differences from browser fetch:

  • Response.formData() and Request.formData() throw (Response method);
  • json() turns an empty buffered body into {} instead of propagating JSON.parse(""), and json() does not drain a progressive stream (JSON method);
  • bytes() does not drain a progressive stream, and clone() does not copy its stream (binary and clone methods);
  • native network failures are platform-originated errors. Android wraps Cronet failures as RuntimeException("Cronet failed: …"); iOS propagates URLSession errors. Only aborts receive a normalized name (Android failure path, iOS failure path).

The shared adapter should normalize only the error fields the applications actually consume; it should retain the original error as cause for logs.

Prefetch behavior and limits

prefetch and prefetchOnAppStart require a prefetchKey. A later fetch must send the same key; Nitro removes that internal header before the network request. Successful cache hits add nitroPrefetched: true (prefetch API, native header filtering).

The cache is indexed only by prefetchKey, not by URL, body, headers, or user. The default freshness window is five seconds. Therefore every key must include the complete logical request identity, including the signed-in account when a response is private (Android cache lookup).

There is also a current cross-platform difference: Android removes a cached result when it is consumed, while iOS leaves a fresh result available until it expires (Android cache, iOS cache). Application correctness must never depend on how many times one prefetched result can be consumed.

prefetchOnAppStart schedules the request for the next process start. Its entry includes URL, headers, method, body, credentials, redirect behavior, and prefetch TTL, and replaces an earlier queued entry with the same key (queue serialization). iOS replays the queue automatically. Android's Expo config plugin edits Application.onCreate to call AutoPrefetcher.prefetchOnStart before React loads (iOS startup behavior, Expo plugin).

Queued headers may contain credentials. The storage implementation normally encrypts values with Android Keystore or iOS Keychain-backed AES-GCM, but both platforms deliberately fall back to plaintext when secure key access fails (Android fallback, iOS fallback). Do not persist bearer tokens directly in auto-prefetch headers. The token refresh configuration uses the same storage and may itself contain a request body or long-lived secret, so it does not remove the plaintext-fallback risk. Use authenticated startup prefetch only after a security review of the exact refresh payload, clear both the queue and refresh configuration on logout, and exclude user-private startup prefetch until that lifecycle has integration tests.

Expo installation requirements

For each native Expo application:

  1. Install the same pinned versions of react-native-nitro-fetch and react-native-nitro-modules. Install react-native-worklets only if the app already uses it or adopts measured worklet mapping.
  2. Add react-native-nitro-fetch to the Expo config plugins when the app uses prefetchOnAppStart; the shipped plugin only changes Android startup. This plugin configuration is inferred from the published app.plugin.js export and plugin source (plugin export, plugin implementation).
  3. Regenerate native projects as required by the app, install pods, and rebuild every development client. The official getting-started guide explicitly requires rebuilding after installation (getting started).
  4. Verify both iOS and Android release builds. A JavaScript test proves adapter behavior but cannot prove Cronet, URLSession, cookie, abort, upload, or startup-prefetch integration.

Testing and mocking

Nitro Fetch does not publish a Jest mock or request-mocking API. The package excludes __tests__, __fixtures__, and __mocks__ from the npm artifact, and its two index test files are only it.todo (published-file rules, index test). The repository does contain native-device harnesses, but consumers cannot use those as an application mock.

Test at three boundaries:

  • unit-test URL, headers, auth, error, timeout, and JSON behavior by injecting a fetch-compatible function into the shared adapter;
  • mock the shared adapter, not react-native-nitro-modules, in feature/query tests;
  • run native integration coverage for cancellation, each body family, cookies where used, binary downloads, streaming endpoints, and startup prefetch.

This testing boundary is a recommendation inferred from the package's eager native singleton and lack of a shipped mock.

Migration order

  1. Add the platform-specific shared transport and a typed adapter without changing feature call sites.
  2. Port ordinary JSON GET/POST calls first. Preserve every existing status and parsing contract, and forward TanStack Query's signal as the official integration example does (TanStack Query example).
  3. Port binary downloads, local files, and multipart uploads behind explicit adapter methods and native integration tests.
  4. Port streaming endpoints only with stream: true; do not globally add that flag.
  5. Audit cookie-backed auth separately on Android before enabling cookie sync.
  6. Enable intent-time prefetch only for measured navigation bottlenecks.
  7. Enable prefetchOnAppStart last, after auth/logout behavior and platform-specific key semantics are tested.
  8. Remove direct global-fetch use from application-owned API modules once the inventory is empty. Third-party libraries should receive the adapter's fetch function where they expose one; use a native-only global replacement only for a library that offers no injection seam.

The official docs show a global replacement and say it also affects third-party libraries (global replacement guide). For these cross-platform Expo applications, the source-level web recursion and partial Request/Response surface make that a last-resort compatibility mode, not the default architecture.

Shared-chat claim check

The supplied shared ChatGPT conversation correctly identifies Margelo's react-native-nitro-fetch and says prefetchOnAppStart queues work for the next cold launch. The official README confirms both points (package and next-launch behavior, startup prefetch).

The shared chat also mentions an Expensify startup improvement of approximately 950 ms. That number was not present in the official source, README, benchmark page, or package metadata reviewed here, so it must not be used as a migration acceptance target. Measure each StatsHub and Pitsi Labs app independently in a release build.

On this page