react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-10-01 · react + react-native

Networking & HTTP client layer

reviewedconfidence: mediumtrack: heldthis tier holds 71% on the public scorecard (7/20 graded · 0 overturned) →

related decisions: data · ai-ui · p2p

re-verified 8× — 2026-10-08 · 2026-09-24 · 2026-09-01 · 2026-08-18 · 2026-07-17 · 2026-07-16 +2 · changelog

recommendation

fetch (built-in) on both platforms: the browser's native fetch on react; on react-native the built-in fetch, which on Expo SDK 56+ is already expo/fetch globally. The bigger win in most apps is the CACHE layer above it (TanStack Query, RB-E-DATA), not the client. The pick changes to react-native-nitro-fetch only when the network layer is a MEASURED hot path (cold-start requests, media, HTTP/3 backends) or you need prefetch/worklet parsing.

  • team already on axios → axios (keep it; interceptor ergonomics are real and migration buys little)
  • web team already on ky or ofetch → ky / ofetch (keep it; a thin fetch wrapper, same reasoning as axios)
  • streaming responses (AI/chat tokens, SSE-style) → expo/fetch, else react-native-nitro-fetch (streaming + TextDecoder; see RB-E-AI-UI)
  • measured network hot path / prefetch-before-navigation / HTTP-3 backend on react-native → react-native-nitro-fetch (drop-in)
  • server-state caching, retries, invalidation → TanStack Query (RB-E-DATA), regardless of client
  • P2P / Holepunch app → n/a (there may be no HTTP layer at all; transport is Hyperswarm streams, RB-E-P2P)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
fetch (built-in)the default on both platforms — RN's WHATWG polyfill over native networking, the browser's native fetch on web; fine for most apps166k/wk · ships in 2/34
expo/fetchWinterTC-compliant fetch with streaming; installed as the global fetch since Expo SDK 56 (no import needed; opt out with EXPO_PUBLIC_USE_RN_FETCH=1) — the modern managed default (verified vs the SDK 56 changelog)
react-native-nitro-fetch (Margelo)drop-in native fetch on Cronet (Android) / URLSession (iOS): HTTP/1-2-3+QUIC, Brotli, disk cache, prefetch, worklet parsing off the JS thread. Vendor harness ~1.3x (~23%) vs built-in; Expensify in production: 15-30% faster requests, −267ms iOS / −650ms Android P50 warm start. 1.8.0 (2026-09-30, verified vs npm + GitHub) adds formData() support for urlencoded Request/Response bodies and fixes timeoutMs forwarding + abort-reason propagation, RN 0.75+; see note: NITRO-FETCH166k/wk · ships in 2/34
axiosthe ubiquitous promise client (interceptors, defaults, wide team familiarity); runs over XHR in RN — no reason to churn OFF it, little reason to start ON it for new work. 1.20.0 (2026-08-26, verified vs npm)139.9M/wk · ships in 8/34
ky / ofetchsmall fetch wrappers (retries, hooks, typed convenience), web-leaning. ky 2.1.0 (2026-08-28), ofetch (unjs) 1.5.1 (2025-11-01), verified vs npm34.9M/wk

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): axios 139.9M · react-native-nitro-fetch 166k · ky 8.6M · ofetch 34.9M

Verified notes

The HTTP client is the least consequential of three layers. RN's built-in fetch is a JS polyfill over native networking; expo/fetch standardizes that API (WinterTC + streaming); nitro-fetch swaps the ENGINE (Cronet/URLSession, HTTP/3) behind the same API. Every serious client is fetch-shaped, so client choice is low-stakes compared to cache design above it (RB-E-DATA), and only the engine swap below it buys measurable speed — which is why the default is plain fetch and the engine swap is gated on a measured hot path. NITRO-FETCH (2026-09-24, verified vs npm, its README and the Margelo/Expensify blog): the vendor harness measures 23.2% average / 1.30× (10 runs, release build, Wi-Fi, no prefetch). Expensify's global replacement of fetch/Headers/Request/Response gave 15-30% faster requests; prefetching the one critical startup request before the JS runtime loads (account-scoped prefetch keys, pre-runtime token refresh) moved P50 warm start 1,676→1,409ms on iOS and 3,223→2,573ms on Android. On Android Cronet you own certificate pinning. Floors (1.7.0 peer dependencies): react-native-nitro-modules ≥0.36.1; react-native-worklets ≥0.8.0, optional, needed only for worklet parsing. ABORT MODERNIZATION (2026-09-24, verified vs commit 6f3375a and the GitHub compare against v0.87.0): RN replaced the dormant abort-controller polyfill with an in-tree fork wired to RN's own EventTarget (dropping event-target-shim) and added AbortSignal.timeout(), AbortSignal.any(), AbortSignal.abort(reason) and signal.throwIfAborted(). It shipped in RN 0.87.0 → on RN ≥0.87, web cancellation patterns (per-request deadlines, merged signals) work unmodified. CROSS-REFERENCES: RB-E-DATA owns caching, retries and invalidation; RB-E-AI-UI owns the streaming-token UI; RB-E-P2P owns the no-HTTP lane. HISTORY: 2026-10-01 REOPENED: nitro-fetch 1.8.0 pin (formData() for urlencoded bodies; timeoutMs/abort-reason fixes). 2026-07-09 GAP CLOSED: the fetch-layer choice had no home; entry promoted from the Native Weekly first harvest (nitro-fetch featured twice; SDK 56 made expo/fetch the global default).

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

react-native-nitro-fetch — architecture & benchmarksMargelo

The README doubles as the layer's best explainer: what actually backs fetch on each platform (Cronet vs URLSession), what HTTP/3+QUIC/Brotli/disk-cache buy you, prefetch-before-navigation, and worklet-side parsing — with their benchmark methodology. Read it to understand the layer even if you keep built-in fetch.

Speeding up Expensify's networking with NitroFetchMargelo (with Expensify)

The first at-scale production receipt for the fetch-engine swap (vendor-written — Margelo builds nitro-fetch — but the numbers are Expensify's app): global replacement of fetch/Headers/Request/Response gave 15-30% faster requests; prefetching the single critical startup request (with token refresh BEFORE the JS runtime loads, account-scoped prefetch keys to prevent cross-account reuse) cut P50 startup 1,676→1,409ms on iOS and 3,223→2,573ms on Android. Also documents the sharp edge: Cronet needed a custom certificate-pinning implementation. Grounds this entry's 'reach for nitro-fetch when measured' clause with what MEASURED wins look like.

Sources

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About networking & the HTTP client layer

Diataxis: Explanation. This page builds understanding of the networking decision — why the HTTP client itself is the least consequential of three layers, and where the real decisions sit. It is not a fetch API guide and not a migration recipe. The candidate list and one-line tradeoffs live in the index entry RB-E-NETWORKING; the entry declares no defer skill, but the decision it keeps pointing at — server-state caching — is owned by RB-E-DATA. Read this for the why.

The one move that organises everything: look above and below the client, not at it

The entry's note lays out three layers in one sentence. RN's built-in fetch is a JS polyfill over native networking; expo/fetch standardizes that same API (WinterTC + streaming); nitro-fetch swaps the engine (Cronet/URLSession, HTTP/3) behind the same API. So the stack reads:

  • Above the client: the cache. The recommendation's own words: "the bigger win in most apps is the CACHE layer above it (TanStack Query, RB-E-DATA), not the client." Server-state caching, retries, invalidation — TanStack Query territory regardless of client.
  • The client itself: a fetch-shaped commodity. Every serious option converges on the same API surface; the entry's judgment is that client choice is "low-stakes compared to cache design."
  • Below the client: the engine. What actually moves the bytes — Cronet on Android, URLSession on iOS, HTTP/3+QUIC, Brotli, disk cache. This is the only layer where swapping buys measurable speed, and the entry gates that swap on measurement.

The decision, then, is rarely "which client." It is whether the cache layer above exists, and whether a measured hot path justifies touching the engine below.

The default, and why

fetch (built-in) on both platforms: the browser's native fetch on react; on react-native the built-in fetch, which on Expo SDK 56+ is already expo/fetch globally. The bigger win in most apps is the CACHE layer above it (TanStack Query, RB-E-DATA), not the client. The pick changes to react-native-nitro-fetch only when the network layer is a MEASURED hot path (cold-start requests, media, HTTP/3 backends) or you need prefetch/worklet parsing.

The default is to do nothing. fetch is already the default on both platforms — RN's WHATWG polyfill over native networking, the browser's native fetch on web — and it is fine for most apps. On Expo SDK 56+ the middle layer has even been upgraded for you: expo/fetch is WinterTC-compliant with streaming and installed as the global fetch, no import needed — the modern managed default, verified against the SDK 56 changelog.

The two escalations are asymmetric. Downward is gated: reach for nitro-fetch only when the network layer is a measured hot path (cold-start requests, media, HTTP/3 backends) or you need prefetch/worklet parsing. Upward is not gated at all: caching, retries, and invalidation belong to TanStack Query whatever client you keep.

The landscape, layer by layer

fetch (built-in) — the default on both platforms: RN's WHATWG polyfill over native networking, the browser's native fetch on web. Fine for most apps.

expo/fetch — WinterTC-compliant fetch with streaming, installed as the global fetch since Expo SDK 56 (no import needed). The modern managed default; if you are on SDK 56+ you are already using it.

react-native-nitro-fetch (Margelo) — the engine swap: drop-in native fetch on Cronet (Android) / URLSession (iOS), bringing HTTP/1-2-3+QUIC, Brotli, disk cache, prefetch-before-navigation, and worklet parsing off the JS thread. 1.7.0 (2026-09-12), RN 0.75+. Its own harness measures ~23% (1.30×) over built-in fetch — one number, stated two ways, vendor-run. The production calibration point is Expensify's rollout: 15-30% faster requests from the global swap, and a P50 warm-start cut of 1,676→1,409ms on iOS and 3,223→2,573ms on Android from prefetching the one critical startup request before the JS runtime loads. The same rollout names the sharp edge: on Android, Cronet needed a custom certificate-pinning implementation. Measured wins like these are what the entry's gate asks for. Its README doubles as the layer's best explainer — what actually backs fetch on each platform, what HTTP/3+QUIC/Brotli/disk-cache buy, the benchmark methodology — and the entry says to read it even if you keep built-in fetch.

axios — the ubiquitous promise client: interceptors, defaults, wide team familiarity; it runs over XHR in RN. The entry's verdict is symmetric and calm: no reason to churn off it, little reason to start on it for new work.

ky / ofetch — small fetch wrappers (retries, hooks, typed convenience), web-leaning; ky 2.x, ofetch (unjs). Same verdict as axios: a team already on one keeps it.

Tradeoffs and failure modes to name out loud

  • Optimizing the commodity. Swapping HTTP clients while no cache layer exists above them. Client choice is low-stakes compared to cache design — the churn buys ergonomics at best while the actual win (caching, retries, invalidation) sits unclaimed in RB-E-DATA.
  • Unmeasured engine swaps. nitro-fetch is drop-in, which makes adoption cheap — but the recommendation gates it on a measured hot path, and the entry flags the benchmarks as vendor-run. Cheap to adopt is not the same as necessary.
  • Churning axios in either direction. Migrating a team off axios buys little — the interceptor ergonomics are real. Starting new work on it also buys little. Both moves are motion without progress.
  • Rebuilding the cache inside the client. Hand-rolled retry/cache/invalidation logic in interceptors is TanStack Query territory implemented worse, regardless of which client hosts it.
  • Hand-rolled cancellation on old RN. Since RN 0.87.0 the in-tree AbortController supports AbortSignal.timeout(), AbortSignal.any(), AbortSignal.abort(reason) and signal.throwIfAborted(), so web cancellation patterns (per-request deadlines, merged signals) work unmodified; below 0.87 those four APIs are missing.
  • Raw XMLHttpRequest. The entry's source-level smell: raw XHR bypasses the fetch layer's streaming/abort semantics. Prefer fetch or the app's chosen client.
  • Streaming through the wrong client. AI/chat tokens (SSE-style) need a streaming-capable client: expo/fetch or nitro-fetch (streaming + TextDecoder).
  • Assuming HTTP exists at all. In a P2P/Holepunch app there may be no HTTP layer — transport is Hyperswarm streams (RB-E-P2P).

How it interacts with the rest of the stack

  • Server state (RB-E-DATA). The layer above, and the entry's repeated pointer: the bigger win is the cache — TanStack Query for caching, retries, invalidation, regardless of client.
  • AI interfaces (RB-E-AI-UI). The streaming lane: token-by-token responses route to expo/fetch or nitro-fetch, per the entry's when-clause.
  • P2P (RB-E-P2P). The no-HTTP lane: Holepunch transport is Hyperswarm streams; this entire layer may be absent.
  • No defer skill. The entry declares none — the depth it defers is decisional (to RB-E-DATA), not a specialist audit.

In one paragraph

The HTTP client is a commodity: every serious option is fetch-shaped, and the real decisions sit above it (the cache) and below it (the engine). Use fetch — on Expo SDK 56+ you are already on expo/fetch globally, WinterTC-compliant with streaming — and spend the attention on TanStack Query (RB-E-DATA), because caching, retries, and invalidation are where most apps win, regardless of client. The engine swap, react-native-nitro-fetch (Cronet/URLSession: HTTP/3+QUIC, Brotli, disk cache, prefetch, worklet parsing), is drop-in but gated on a measured hot path — its harness number (~23%, 1.30×) is vendor-run, and Expensify's 15-30% faster requests is the production calibration point. axios earns a symmetric verdict: no reason to churn off it, little reason to start on it. Streaming (AI/chat tokens) routes to expo/fetch or nitro-fetch; raw XHR is a smell that bypasses streaming/abort semantics; and in a P2P app the whole layer may not exist — transport is Hyperswarm streams.


See also: RB-E-DATA (the cache layer above — TanStack Query for caching, retries, invalidation), RB-E-AI-UI (streaming token responses), RB-E-P2P (Hyperswarm streams — the no-HTTP lane). Background reading: the react-native-nitro-fetch README — the layer's best explainer of what backs fetch on each platform, worth reading even if you keep built-in fetch.

Related in app-architecture: state · data · p2p · nav · meta-frameworks · forms · auth · uploads · crossplatform · desktop