Skip to content

Native Networking with Nitro Fetch

Nitro Fetch by Margelo is a drop-in replacement for fetch that sends requests through the platform’s native HTTP stack: URLSession on iOS and Cronet on Android. The starter uses it under the Axios client, so your API hooks get native networking without any code changes.

React Native’s built-in networking goes through a JavaScript bridge layer. Nitro Fetch hands requests directly to the native client, which brings:

  • HTTP/2 and HTTP/3 (QUIC) support, plus Brotli compression
  • Disk caching handled by the native stack
  • Less work on the JavaScript thread, since the request and response handling happen natively
  • Prefetching, including requests that start before your JavaScript bundle runs

The Axios client in src/lib/api/client.tsx switches to Nitro Fetch through Axios’ built-in fetch adapter:

src/lib/api/client.tsx
import type { AxiosRequestConfig } from 'axios';
import Env from '@env';
import axios from 'axios';
import { Platform } from 'react-native';
import { fetch as nitroFetch } from 'react-native-nitro-fetch';
// Release builds on iOS and Android send requests through react-native-nitro-fetch,
// a native HTTP stack (URLSession on iOS, Cronet on Android) with HTTP/2, HTTP/3 and
// disk caching. Dev builds keep axios' default XHR adapter, because network inspectors
// such as React Native DevTools only see XHR traffic. Web uses the browser stack.
// Set this to `true` temporarily to test the native stack in a dev build.
const USE_NITRO_FETCH = !__DEV__ && Platform.OS !== 'web';
const nitroFetchConfig = {
adapter: 'fetch',
env: {
fetch: nitroFetch,
// `null` makes axios pass the url and options straight to Nitro's native client
// instead of wrapping them in JS Request/Response objects.
Request: null,
Response: null,
},
} as unknown as AxiosRequestConfig;
export const client = axios.create({
baseURL: Env.EXPO_PUBLIC_API_URL,
...(USE_NITRO_FETCH ? nitroFetchConfig : {}),
});
BuildNetwork stack
Release, iOS and AndroidNitro Fetch (URLSession, Cronet)
DevelopmentAxios’ default XHR adapter
WebThe browser’s own network stack

Development builds keep XHR on purpose: network inspectors such as React Native DevTools only see XHR traffic. Release builds, which your users run, get the native stack.

Setting Request and Response to null in the adapter config tells Axios to pass the URL and options straight to Nitro’s native client, instead of wrapping them in JavaScript Request and Response objects first.

Nothing changes for the code that calls the API. The hooks in src/features/*/api.ts keep using client as before. See Data Fetching for how those hooks are built.

To try Nitro Fetch in a development build, temporarily set USE_NITRO_FETCH to true in src/lib/api/client.tsx and reload the app. The native module is already part of every build, so no rebuild is needed.

To test exactly what users get, run a release build locally:

Terminal window
bunx expo run:ios --configuration Release
bunx expo run:android --variant release

On Android, you can confirm that Nitro Fetch is active by looking for Cronet in the device logs:

Terminal window
adb logcat -d | grep -i cronet

You can also call Nitro Fetch without Axios. It has the same API as the built-in fetch:

import { fetch } from 'react-native-nitro-fetch';
const res = await fetch('https://dummyjson.com/posts');
const json = await res.json();

Prefetching starts a request before your code needs the response. Nitro Fetch keeps the result in its native cache, and the next fetch call with the same prefetchKey header resolves immediately instead of waiting for the network.

Start a request when the user is about to open a screen, for example when they press a list item. The screen’s own request then finds the data ready:

import { router } from 'expo-router';
import { prefetch } from 'react-native-nitro-fetch';
import Env from '@env';
function openPost(id: number) {
// Fire and forget: navigation does not wait for the request
prefetch(`${Env.EXPO_PUBLIC_API_URL}posts/${id}`, {
headers: { prefetchKey: `post-${id}` },
});
router.push(`/feed/${id}`);
}

On the new screen, request the same URL with the same prefetchKey:

import { fetch } from 'react-native-nitro-fetch';
import Env from '@env';
const res = await fetch(`${Env.EXPO_PUBLIC_API_URL}posts/${id}`, {
headers: { prefetchKey: `post-${id}` },
});
// "nitroPrefetched" tells you whether the response came from the prefetch
console.log(res.headers.get('nitroPrefetched'));

prefetchOnAppStart queues a request for the next launch. Native code sends it while the app starts, before your JavaScript bundle runs, so data like the home feed is ready on the first render. Margelo measured about 220 ms faster time-to-interactive from prefetching alone.

import { prefetchOnAppStart } from 'react-native-nitro-fetch';
await prefetchOnAppStart(`${Env.EXPO_PUBLIC_API_URL}posts`, {
prefetchKey: 'feed',
});

When the app opens next time, a fetch with headers: { prefetchKey: 'feed' } uses the prefetched response.

On Android, app-start prefetching needs Nitro Fetch’s config plugin, which hooks the prefetcher into the app’s startup code. iOS picks the queue up automatically. Add the plugin to app.config.ts and run bun run prebuild:

app.config.ts
plugins: [
// ...
'react-native-nitro-fetch',
],

If those startup requests need an auth token, Nitro Fetch can refresh it natively before sending them. See registerTokenRefresh in the Nitro Fetch documentation.

Nitro Fetch can also parse responses on a worklet with nitroFetchOnWorklet, so large JSON payloads do not block the JavaScript thread, and stream response bodies. See the Nitro Fetch documentation.

Native modules do not exist in Jest, so jest-setup.ts mocks react-native-nitro-fetch. Tests run in development mode, where the client uses XHR anyway.

If you prefer the default networking stack:

  1. Remove the react-native-nitro-fetch import and the nitroFetchConfig spread from src/lib/api/client.tsx.
  2. Run bun remove react-native-nitro-fetch and delete its mock from jest-setup.ts.
  3. Remove it from expo.doctor.reactNativeDirectoryCheck.exclude in package.json.
  4. Rebuild the app with bun run prebuild.

Keep react-native-nitro-modules, because MMKV depends on it.