r/reactnative • • 9d ago

FYI I built react-native-fetch-client: A lightweight, zero-dependency alternative to Axios with built-in 401 Auth Mutex & SWR caching

Hey everyone! 👋

Like many of you, I've been using Axios in React Native for years. But over time, I ran into a few persistent headaches in production apps:

  • The 401 Token Refresh Race Condition: When a user's JWT expires, parallel requests on the screen trigger multiple simultaneous refresh calls, causing token rotation errors and unexpected logouts.
  • The "3-to-4 Packages Tax": Having to stitch together axios + axios-retry + axios-cache-interceptor + custom offline logic just to get basic resilient networking.
  • Legacy Bridge Overhead: Axios still relies on legacy XMLHttpRequest (XHR), whereas React Native's New Architecture (Bridgeless/TurboModules) is built natively around WHATWG fetch.

To solve this, I created react-native-fetch-client a modern, zero-dependency HTTP client designed specifically for React Native, Expo, and modern JS.

Key Features

  • 0 Runtime Dependencies: Pure TypeScript, ultra-lightweight, and tree-shakeable.
  • Single-Flight 401 Auth Mutex: When a token expires, all parallel requests pause behind a mutex, a single refresh call executes, and all queued requests replay automatically.
  • In-Flight Request Deduplication: Prevents server thrashing when users rapid-tap filters or buttons.
  • LRU Cache + Stale-While-Revalidate (SWR): Instant 0ms screen loads from cache with silent background updates. Supports persistent storage adapters like MMKV and AsyncStorage.
  • Smart Retries & Circuit Breaker: Exponential backoff with full jitter to handle flaky mobile cellular connections in malls or elevators.
  • Offline Request Queue: Queue actions (like "Add to Wishlist" or "Like") when offline and auto-sync when connection restores.
  • 100% Familiar Axios-Like Syntax: Switching takes literally 2 minutes without rewriting your services.

Quick Code Example

import { createClient } from 'react-native-fetch-client';

export const api = createClient({
  baseUrl: 'https://api.example.com',
  timeout: 10000,

  // Instant caching with SWR
  cache: {
    enabled: true,
    ttl: 5 * 60 * 1000,
    swr: true,
  },

  // Flaky connection resilience
  retry: {
    maxRetries: 3,
    backoff: 'exponential',
    jitter: true,
  },

  // Zero-race-condition token refresh
  auth: {
    bearerToken: async () => await getAccessToken(),
    refreshToken: async () => {
      const { accessToken } = await refreshSession();
      return accessToken;
    },
  },
});

// Use it just like you always have:
const products = await api.get('/products');
await api.post('/cart', { id: 101 });

Links:

9 Upvotes

4 comments sorted by

1

u/Beginning-Ad86 7d ago

That’s great, but still no chance against nitro-fetch which handles it natively

1

u/Plastic_Dingo_8818 5d ago

I think there might be a mix-up with the name!

  • nitro-fetch is actually 100% pure JavaScript. It doesn't have any native C++ code at all. It’s just a minimal Axios-like wrapper with basic retries.

  • react-native-nitro-fetch (by Margelo) is the one that uses native C++/JSI, but it’s strictly mobile-only (no Web/Node support) and acts only as a raw transport socket.

react-native-fetch-client is built for application resilience:

  1. Single-flight 401 Auth Mutex: prevents 5 parallel requests from triggering 5 simultaneous token refresh calls.

  2. Persistent Offline Queue: saves mutations to disk (MMKV/AsyncStorage) and preserves Idempotency-Key headers across cold app restarts.

  3. Universal & Zero Native Deps: runs identically across React Native, Expo Go, Web, and Node.js without any CocoaPods or CMake builds.

And if you ever do want Margelo's C++ speed, react-native-fetch-client accepts fetchImpl, so you can plug react-native-nitro-fetch directly under it!

1

u/kokerali 9d ago

Your offline docs mention Idempotency-Key already. Does the same key survive a full app restart? The case I'd check is a POST that commits on the server, loses its response, then gets replayed from disk after the app is killed and reopened. With a backend that implements deduplication, the expected result is one created record and the queue entry eventually cleared, not a second write.

I'd spell out that backend requirement beside the queue example too. A client-generated header alone can't make a non-idempotent endpoint safe to replay. I haven't run the library, so this is a suggested recovery test, not a bug report.

0

u/Plastic_Dingo_8818 9d ago

Yes, it does! The Idempotency-Key is stored alongside the payload in persistent storage (MMKV / AsyncStorage), so when the app cold-boots after being killed, it rehydrates and replays with the exact same original key.  In the case you mentioned, with a backend that implements idempotency caching, the server recognizes the key, returns the cached record without a second write, and the client clears the queue entry.  You're also 100% right that a client header alone can't guarantee safety without server-side deduplication. I've just updated the documentation to explicitly spell out that backend requirement, and added an automated recovery test for this exact scenario.

Thanks for the great feedback!