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
RequestandResponseare not complete browser implementations, despite the official docs calling the response spec-compliant. BothformData()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
| Runtime | Verified behavior | Migration consequence |
|---|---|---|
| React Native | The 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 stacks | Every native application needs the native dependencies and a rebuilt development client/binary. A JavaScript-only update cannot add it. |
| Android | The package compiles the app's configured minimum/target SDK values and bundles Cronet. Android build | No independent package min-SDK number is declared; compatibility follows each app and its React Native version. |
| iOS and tvOS | The podspec declares both iOS and tvOS using React Native's min_ios_version_supported. Podspec | iOS is supported. tvOS is declared but is outside the requested Expo app migration. |
| Web | The 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 fallbacks | Keep browser fetching on the browser transport. Nitro's native performance and startup-prefetch features do not exist on web. |
| Worklets | nitroFetchOnWorklet uses optional react-native-worklets; without it, the mapper runs on the JavaScript thread. Implementation | Do 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):
| Area | Exports | Notes |
|---|---|---|
| Fetch primitives | fetch, Headers, Request, Response | The intended migration surface. |
| Prefetch | prefetch, prefetchOnAppStart, removeFromAutoPrefetch, removeAllFromAutoprefetch | Native-only. __readAutoPrefetchQueue is also exported, but its leading underscores mark it as an internal diagnostic seam; application code should not depend on it. |
| Worklets | nitroFetchOnWorklet | Optional Worklets integration. |
| Auth for native startup work | registerTokenRefresh, clearTokenRefresh, callRefreshEndpoint, getStoredTokenRefreshConfig | This serves cold-start prefetch/WebSocket prewarm. It is not a general request interceptor. |
| Diagnostics | NetworkInspector, generateCurl, profileFetch | Inspector entries can be subscribed to with onEntry; they observe completed activity and cannot rewrite a request. Inspector API |
| Low-level native object | NitroFetch | Exposes 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()andRequest.formData()throw (Response method);json()turns an empty buffered body into{}instead of propagatingJSON.parse(""), andjson()does not drain a progressive stream (JSON method);bytes()does not drain a progressive stream, andclone()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:
- Install the same pinned versions of
react-native-nitro-fetchandreact-native-nitro-modules. Installreact-native-workletsonly if the app already uses it or adopts measured worklet mapping. - Add
react-native-nitro-fetchto the Expo config plugins when the app usesprefetchOnAppStart; the shipped plugin only changes Android startup. This plugin configuration is inferred from the publishedapp.plugin.jsexport and plugin source (plugin export, plugin implementation). - 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).
- 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
- Add the platform-specific shared transport and a typed adapter without changing feature call sites.
- Port ordinary JSON GET/POST calls first. Preserve every existing status and
parsing contract, and forward TanStack Query's
signalas the official integration example does (TanStack Query example). - Port binary downloads, local files, and multipart uploads behind explicit adapter methods and native integration tests.
- Port streaming endpoints only with
stream: true; do not globally add that flag. - Audit cookie-backed auth separately on Android before enabling cookie sync.
- Enable intent-time
prefetchonly for measured navigation bottlenecks. - Enable
prefetchOnAppStartlast, after auth/logout behavior and platform-specific key semantics are tested. - 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.