Skip to content

margelo/react-native-nitro-fetch

Nitro Modules

react-native-nitro-fetch is a general purpose network fetching library for React Native. It can be used as a drop-in replacement for the built-in fetch(...) method, as well as provide additional features like prefetching and workletized mappers.

Features

Installation

npm i react-native-nitro-fetch react-native-nitro-modules

Nitro Modules requires react-native 0.75+ or higher

WebSockets (optional) — add the companion socket package plus text decoder (peer dependency of websockets):

npm i react-native-nitro-websockets react-native-nitro-text-decoder

Full setup, native hooks, prewarm, and API details: docs/websockets.md · UI: example/src/screens/WebSocketScreen.tsx · auth + prewarm: Token refresh (example block).

Usage

To simply fetch data, import the fetch(...) method from react-native-nitro-fetch:

import { fetch } from 'react-native-nitro-fetch'

const res = await fetch('https://httpbin.org/get')
const json = await res.json()

This can be used as a drop-in-replacement for the built-in fetch(...) method.

Prefetching in JS

You can prefetch a URL in JS, which keeps the result cached for the next actual fetch(...) call - this can be used shortly before navigating to a new screen to have results hot & ready:

import { prefetch } from 'react-native-nitro-fetch'

await prefetch('https://httpbin.org/uuid', {
  headers: { prefetchKey: 'uuid' }
})

Then, on the new screen that was navigated to:

import { fetch } from 'react-native-nitro-fetch'

const res = await fetch('https://httpbin.org/uuid', {
  headers: { prefetchKey: 'uuid' }
})
console.log('prefetched header:', res.headers.get('nitroPrefetched'))

Prefetching for the next app launch

Prefetching data on app launch (or process start) will make it hot & ready once your JS code actually runs. Call prefetchOnAppStart(...) to enqueue a prefetch for the next app start:

import { prefetchOnAppStart } from 'react-native-nitro-fetch'

await prefetchOnAppStart('https://httpbin.org/uuid', {
  prefetchKey: 'uuid'
})

Then, once the app opens the next time, a call to fetch(...) might resolve faster since it will contain already cached results:

import { fetch } from 'react-native-nitro-fetch'

const res = await fetch('https://httpbin.org/uuid', {
  headers: { prefetchKey: 'uuid' }
})
console.log('prefetched header:', res.headers.get('nitroPrefetched'))

In our tests, prefetching alone yielded a ~220 ms faster TTI (time-to-interactive) time! 🤯

Token refresh (cold start)

When you use auto-prefetch (prefetchOnAppStart) and/or WebSocket prewarm on app start (react-native-nitro-websockets), native code runs before your JS bundle. If those requests need auth headers, you can register a token refresh configuration. On each cold start, native code calls your refresh URL, maps the response into HTTP headers, and merges them into auto-prefetches and/or WebSocket prewarms.

1. Register the refresh config (persisted in encrypted native storage):

import { registerTokenRefresh } from 'react-native-nitro-fetch'

registerTokenRefresh({
  target: 'fetch', // 'websocket' | 'fetch' | 'all'
  url: 'https://api.example.com/oauth/token',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ grant_type: 'client_credentials' }),
  responseType: 'json',
  mappings: [
    { jsonPath: 'access_token', header: 'Authorization', valueTemplate: 'Bearer {{value}}' },
  ],
  // If the refresh request fails:
  // - 'useStoredHeaders' — use last successful headers from the previous run (default)
  // - 'skip' — skip auto-prefetch / prewarm entirely when refresh fails
  onFailure: 'useStoredHeaders',
})

Response mapping

  • Default responseType is 'json'. Use mappings to copy fields from the JSON body into header names (dot paths supported, e.g. data.token).
  • Use compositeHeaders to build a header from a template and multiple JSON paths ({{placeholder}} in the template).
  • For a plain-text body, set responseType: 'text' and use textHeader / optional textTemplate (with {{value}}).

Example: token refresh + WebSocket prewarm

import { registerTokenRefresh } from 'react-native-nitro-fetch'
import { prewarmOnAppStart, NitroWebSocket } from 'react-native-nitro-websockets'

const WSS = 'wss://api.example.com/live'

registerTokenRefresh({
  target: 'websocket', // use 'all' if you also use prefetchOnAppStart with the same token flow
  url: 'https://api.example.com/oauth/token',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'client_credentials',
    client_id: '…',
    client_secret: '…',
  }),
  mappings: [
    { jsonPath: 'access_token', header: 'Authorization', valueTemplate: 'Bearer {{value}}' },
  ],
})

3. Optional JS helpers

import {
  callRefreshEndpoint,
  clearTokenRefresh,
  getStoredTokenRefreshConfig,
} from 'react-native-nitro-fetch'

// Same mapping rules as native; uses global fetch from JS
const headers = await callRefreshEndpoint(config)

// Remove stored config and token caches (scope with 'fetch' | 'websocket' | 'all')
clearTokenRefresh('fetch')

// Read back what was registered (or null)
const stored = getStoredTokenRefreshConfig('fetch')

The refresh config and header caches are stored with platform secure storage (Android Keystore + encrypted values in SharedPreferences, iOS Keychain-backed encryption in the same UserDefaults suite as other nitro keys).

AbortController

Cancel in-flight requests using the standard AbortController API:

import { fetch } from 'react-native-nitro-fetch'

const controller = new AbortController()

// Abort after 500ms
setTimeout(() => controller.abort(), 500)

try {
  const res = await fetch('https://httpbin.org/delay/20', {
    signal: controller.signal,
  })
} catch (e) {
  if (e.name === 'AbortError') {
    console.log('Request was cancelled')
  }
}

Pre-aborted signals are also supported — the request will throw immediately without making a network call:

const controller = new AbortController()
controller.abort()

await fetch(url, { signal: controller.signal }) // throws AbortError

FormData

Upload files and form fields using FormData:

import { fetch } from 'react-native-nitro-fetch'

const fd = new FormData()
fd.append('username', 'nitro_user')
fd.append('avatar', {
  uri: 'file:///path/to/photo.jpg',
  type: 'image/jpeg',
  name: 'avatar.jpg',
} as any)

const res = await fetch('https://httpbin.org/post', {
  method: 'POST',
  body: fd,
})
const json = await res.json()

Worklet Mapping

Since Nitro Fetch is a Nitro Module, it can be used from Worklets. This can be useful to parse data without blocking the main JS-Thread:

import { nitroFetchOnWorklet } from 'react-native-nitro-fetch'

const data = await nitroFetchOnWorklet(
  'https://httpbin.org/get',
  undefined,
  (payload) => {
    'worklet'
    return JSON.parse(payload.bodyString ?? '{}')
  }
)

Before using worklet mapping, install and configure react-native-worklets.

Streaming with TextDecoder

Nitro Fetch can also expose an streaming mode that returns a ReadableStream body.
Combined with react-native-nitro-text-decoder, you can incrementally decode UTF‑8 chunks:

import { useRef, useState } from 'react'
import { fetch as nitroFetch } from 'react-native-nitro-fetch'
import { TextDecoder } from 'react-native-nitro-text-decoder'

export function StreamingExample() {
  const [output, setOutput] = useState('')
  const decoder = useRef(new TextDecoder())

  const append = (text: string) => {
    setOutput(prev => prev + text)
  }

  const runStream = async () => {
    // `stream: true` enables the streaming transport
    const res = await nitrofetch('https://httpbin.org/stream/20', {
      stream: true,
    })

    const reader = res.body?.getReader()
    if (!reader) {
      append('No readable stream!')
      return
    }

    let chunks = 0
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      chunks++
      const text = decoder.current.decode(value, { stream: true })
      append(text)
    }

    append(`\n\n✅ Done — ${chunks} chunk(s) received`)
  }

  // Call `runStream()` from a button handler in your UI
}

WebSockets & prewarm

Use react-native-nitro-websockets for NitroWebSocket (browser-like API: onopen, onmessage, send, close, …). Install react-native-nitro-text-decoder alongside it — the socket package uses it to decode UTF-8 text frames.

Prewarm on next launch — queue URLs from JS so native code can start the handshake before React loads:

import {
  prewarmOnAppStart,
  removeFromPrewarmQueue,
  clearPrewarmQueue,
} from 'react-native-nitro-websockets'

prewarmOnAppStart('wss://echo.websocket.org')
// optional: prewarmOnAppStart(url, ['subproto'], { Authorization: 'Bearer …' })

clearPrewarmQueue()
removeFromPrewarmQueue('wss://echo.websocket.org')

On Android, call NitroWebSocketAutoPrewarmer.prewarmOnStart(this) in Application.onCreate (see example MainApplication.kt). iOS picks up the queue via the linked pod.

Authenticated prewarms: use registerTokenRefresh with target: 'websocket' or 'all' — see Token refresh (cold start) for a small registerTokenRefresh + prewarmOnAppStart + NitroWebSocket example.

More detail: docs/websockets.md · UI sample: example/src/screens/WebSocketScreen.tsx.

Project Status

Nitro Fetch is currently in an alpha stage. You can adopt it in production, but keep in mind that the library and it's API is subject to change.

Limitations & Alternatives

Documentation

Margelo

Nitro Fetch is built with ❤️ by Margelo. We build fast and beautiful apps. Contact us at margelo.com for high-end consultancy services.

Contributing

  • Development workflow: CONTRIBUTING.md#development-workflow
  • Sending a pull request: CONTRIBUTING.md#sending-a-pull-request
  • Code of conduct: CODE_OF_CONDUCT.md

Authors

License

MIT