Platform Reality Check

react-native-fetch-client vs. Native Fetch

WHATWG window.fetch is an excellent low-level transport primitive, but it was never designed to handle mobile networking realities like sudden cellular handoffs, token expiry race conditions, and offline caching.


1. Why Raw Fetch Fails in Production Mobile Apps

Browsers run on stable fiber-optic connections; mobile apps run on moving trains, elevators, and spotty cellular networks. Using raw fetch() directly in React Native components introduces serious reliability issues:

No Default Timeouts
Raw Fetch: Hangs indefinitely if connection drops in a tunnel or elevator, silently draining mobile battery and sockets.
Our Solution: Global & per-request timeout with automatic abort signal synthesis.
Does NOT Throw on HTTP 4xx / 5xx
Raw Fetch: Fetch treats 404, 500, and 503 as successful resolutions. Requires manual response.ok and double-await (res.json()).
Our Solution: Structured HttpError classes with status codes and typed response payload already unpacked.
No Single-Flight 401 Auth Mutex
Raw Fetch: When an access token expires, all active parallel requests trigger multiple simultaneous refresh operations, leading to race conditions and unexpected logouts.
Our Solution: Atomic mutex lock pauses concurrent in-flight requests, executes exactly 1 refresh mutation, and replays all pending flights.
No Request Deduplication
Raw Fetch: Duplicate GET requests from multiple mounted React components hit the mobile radio independently, wasting cellular bandwidth.
Our Solution: In-flight promise coalescing shares identical ongoing requests across components.
No Exponential Retry with Jitter
Raw Fetch: Network blips cause immediate UI error screens unless you manually write recursive backoff logic.
Our Solution: Built-in exponential backoff with full jitter, status code predicates, and Retry-After header parsing.
No Offline Queue or SWR Caching
Raw Fetch: All state is lost when offline. Actions fail immediately with "Network request failed".
Our Solution: Built-in SWR cache with MMKV/AsyncStorage persistence and sequential offline mutation queue.

2. Code Comparison: Production Readiness

Notice the staggering difference in defensive boilerplate required to safely execute a single authenticated GET request with timeouts, errors, and unwrap logic:

Raw Fetch (65+ lines of defensive boilerplate)
raw-fetch.ts
typescript
// Native Fetch: 65+ lines of defensive boilerplate required for ONE request
async function fetchUserProfile(userId: string) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 8000); // Manual timeout

  try {
    const token = await getAuthToken(); // Manual token fetch
    const response = await fetch(`https://api.example.com/users/${userId}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      signal: controller.signal,
    });

    clearTimeout(timeoutId);

    // Native fetch does NOT throw on 4xx/5xx errors!
    if (!response.ok) {
      let errorBody = {};
      try {
        errorBody = await response.json();
      } catch {}
      
      // If 401: Manual refresh token handling required here,
      // but without a mutex, 5 concurrent calls will all refresh simultaneously!
      throw new Error(errorBody.message || `HTTP error ${response.status}`);
    }

    const data = await response.json(); // Second await required
    return data;
  } catch (error: any) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('Request timed out after 8000ms');
    }
    // No automatic retry, no offline queue, no request deduplication!
    throw error;
  }
}
react-native-fetch-client (Declarative & clean)
client.ts
typescript
// react-native-fetch-client: 6 lines, fully resilient
import { createClient } from 'react-native-fetch-client';

const api = createClient({
  baseURL: 'https://api.example.com',
  timeout: 8000,                          // Global timeout
  bearerToken: () => getAuthToken(),       // Dynamic auth
  retry: { maxAttempts: 3, backoff: 'exponential' }, // Auto retry on drop
  dedupe: true,                            // In-flight deduplication
  auth: { refresh: () => refreshSession() }, // Atomic 401 single-flight mutex
});

// Clean, safe execution
const [user, error] = await api.safe.get('/users/{userId}', {
  pathParams: { userId },
});

3. Built On WHATWG Fetch, Not a Replacement

react-native-fetch-client does not replace the native fetch standard — it encapsulates and enhances it. Under the hood, every request dispatches directly through the host platform's native WHATWG Fetch implementation (Hermes C++ bindings on React Native, native window.fetch in browsers, and Undici in Node.js 18+).

0 PolyfillsZero legacy XMLHttpRequest overhead or bridge serialization.
Streaming ReadyNative support for Server-Sent Events (SSE) and AI token streams.
Zero DependenciesAll resilience layers are authored directly in ~15.6 KB min+gzip.
Best of Both Worlds
You retain 100% of the native performance and web-standard ergonomics of Fetch, while gaining the mission-critical resilience layers required to run smoothly in mobile production.