Adapters

React Native Adapter

The React Native adapter is optimized for mobile applications built with React Native and Expo. Base RN support covers buffered requests, retries, timeout/abort, hooks, manual redirect handling, and Rezo-managed cookies over the native fetch layer. Optional providers add file downloads, file uploads, readable streams, network-state preflight, and background-task lifecycle support without making those packages mandatory.

import rezo from 'rezo/adapters/react-native';

Import and Setup

import rezo, {
  Rezo,
  RezoError,
  RezoHeaders,
  RezoFormData,
  createReactNativeFsAdapter,
  createNetInfoProvider,
} from 'rezo/adapters/react-native';
import RNFS from 'react-native-fs';
import NetInfo from '@react-native-community/netinfo';

// Default instance
const { data } = await rezo.get('https://api.example.com/users');

// Custom instance for your API
const api = rezo.create({
  baseURL: 'https://api.myapp.com/v1',
  timeout: 15000,
  reactNative: {
    fileSystemAdapter: createReactNativeFsAdapter(RNFS, { background: true }),
    networkInfoProvider: createNetInfoProvider(NetInfo)
  },
  headers: {
    'Accept': 'application/json',
    'X-App-Version': '2.1.0'
  }
});

Environment Detection

The adapter detects the React Native and Expo environments at startup:

// Internal detection (happens automatically)
// Environment.isReactNative = navigator.product === 'ReactNative'
// Environment.isExpo = typeof globalThis.expo !== 'undefined'
// Environment.hasFetch = typeof fetch !== 'undefined'
// Environment.hasBlob = typeof Blob !== 'undefined'
// Environment.hasFormData = typeof FormData !== 'undefined'
// Environment.hasAbortController = typeof AbortController !== 'undefined'

This detection is used internally to adjust behavior. For example, the adapter uses React Native’s native FormData implementation when available, which handles file URIs correctly.

Native FormData and File Uploads

React Native’s FormData supports file objects with uri, type, and name properties — the standard way to upload files from the device:

// Image upload from camera roll
const form = new FormData();
form.append('photo', {
  uri: 'file:///data/user/0/com.myapp/cache/image.jpg',
  type: 'image/jpeg',
  name: 'photo.jpg'
});

const { data } = await api.post('/photos/upload', form);

With Image Picker

import * as ImagePicker from 'expo-image-picker';

const result = await ImagePicker.launchImageLibraryAsync({
  mediaTypes: ImagePicker.MediaTypeOptions.Images,
  quality: 0.8
});

if (!result.canceled) {
  const form = new FormData();
  form.append('avatar', {
    uri: result.assets[0].uri,
    type: result.assets[0].mimeType || 'image/jpeg',
    name: 'avatar.jpg'
  });

  await api.post('/users/avatar', form);
}

With Document Picker

import * as DocumentPicker from 'expo-document-picker';

const result = await DocumentPicker.getDocumentAsync({
  type: 'application/pdf'
});

if (result.type === 'success') {
  const form = new FormData();
  form.append('document', {
    uri: result.uri,
    type: result.mimeType,
    name: result.name
  });

  await api.post('/documents/upload', form);
}

Native Blob Handling

The adapter works with React Native’s Blob implementation for binary data:

// Download binary data
const { data: blob } = await api.get('/files/report.pdf', {
  responseType: 'blob'
});

// Use with expo-file-system to save
import * as FileSystem from 'expo-file-system';

const reader = new FileReader();
reader.onload = async () => {
  const base64 = reader.result.split(',')[1];
  await FileSystem.writeAsStringAsync(
    FileSystem.documentDirectory + 'report.pdf',
    base64,
    { encoding: FileSystem.EncodingType.Base64 }
  );
};
reader.readAsDataURL(blob);

Provider-Backed Transfers

Rezo keeps the standard API shape in React Native. When you configure a file-system adapter or stream transport, you still use the normal download, upload, and stream response objects and standard Rezo events.

import rezo, {
  createReactNativeFsAdapter,
  createFetchStreamTransport
} from 'rezo/platform/react-native';
import RNFS from 'react-native-fs';
import { fetch as expoFetch } from 'expo/fetch';

const api = rezo.create({
  reactNative: {
    fileSystemAdapter: createReactNativeFsAdapter(RNFS),
    streamTransport: createFetchStreamTransport(expoFetch)
  }
});

const download = api.download(
  'https://example.com/report.pdf',
  `${RNFS.DocumentDirectoryPath}/report.pdf`
);

download.on('progress', ({ percentage }) => {
  setProgress(percentage);
});

const stream = api.stream('https://api.example.com/events');
stream.on('data', (chunk) => {
  console.log(chunk);
});

Progress Bar Component

import { View, Text } from 'react-native';
import { useState } from 'react';

function DownloadButton({ url }) {
  const [progress, setProgress] = useState(0);
  const [downloading, setDownloading] = useState(false);

  const download = async () => {
    setDownloading(true);
    const client = rezo.create({
      reactNative: {
        fileSystemAdapter: createReactNativeFsAdapter(RNFS)
      }
    });

    try {
      const task = client.download(url, `${RNFS.DocumentDirectoryPath}/download.bin`);
      task.on('progress', ({ percentage }) => setProgress(percentage));
      await new Promise((resolve, reject) => {
        task.once('finish', resolve);
        task.once('error',  reject);
      });
    } finally {
      setDownloading(false);
      setProgress(0);
    }
  };

  return (
    <View>
      <Text onPress={download}>
        {downloading ? `${progress.toFixed(0)}%` : 'Download'}
      </Text>
    </View>
  );
}

Timeout and Abort

The adapter supports timeout and manual abort using AbortController:

// Timeout
const { data } = await api.get('/slow-endpoint', {
  timeout: 10000
});

// Manual abort (e.g., when component unmounts)
const controller = new AbortController();

useEffect(() => {
  const fetchData = async () => {
    try {
      const { data } = await api.get('/data', {
        signal: controller.signal
      });
      setData(data);
    } catch (error) {
      if (error.code !== 'ECONNABORTED') {
        showError(error.message);
      }
    }
  };

  fetchData();
  return () => controller.abort();
}, []);

Retry Logic

Automatic retries for transient mobile network failures:

const api = rezo.create({
  baseURL: 'https://api.myapp.com',
  retry: {
    limit: 3,
    delay: 1000,
    backoff: 'exponential',
    statusCodes: [408, 429, 500, 502, 503, 504]
  }
});

// Retries automatically on transient failures
const { data } = await api.get('/feed');

This is particularly useful for mobile apps where network connectivity is less reliable than desktop environments.

Response Caching

Cache API responses to reduce network usage on mobile:

const api = rezo.create({
  baseURL: 'https://api.myapp.com',
  cache: {
    response: {
      enable: true,
      ttl: 300_000,     // 5 minutes
      maxEntries: 50
    }
  }
});

// First call hits the network
const { data: config } = await api.get('/app/config');

// Second call returns cached data instantly
const { data: cachedConfig } = await api.get('/app/config');

Authentication Pattern

A common pattern for mobile apps with token-based authentication:

const api = rezo.create({
  baseURL: 'https://api.myapp.com/v1',
  timeout: 15000
});

// Login and store token
async function login(email, password) {
  const { data } = await api.post('/auth/login', { email, password });
  await SecureStore.setItemAsync('token', data.token);
  api.defaults.headers['Authorization'] = `Bearer ${data.token}`;
}

// All subsequent requests include the token
async function getProfile() {
  const { data } = await api.get('/users/me');
  return data;
}

// Handle token expiration
async function refreshableRequest(method, url, body) {
  try {
    return await api[method](url, body);
  } catch (error) {
    if (error.status === 401) {
      const token = await refreshToken();
      api.defaults.headers['Authorization'] = `Bearer ${token}`;
      return await api[method](url, body);
    }
    throw error;
  }
}

Error Handling

try {
  const { data } = await api.get('/protected-resource');
} catch (error) {
  if (RezoError.isRezoError(error)) {
    switch (error.status) {
      case 401:
        // Navigate to login screen
        navigation.navigate('Login');
        break;
      case 403:
        Alert.alert('Access Denied', 'You do not have permission.');
        break;
      case 404:
        Alert.alert('Not Found', 'The resource does not exist.');
        break;
      case 500:
        Alert.alert('Server Error', 'Please try again later.');
        break;
      default:
        if (error.code === 'ETIMEDOUT') {
          Alert.alert('Timeout', 'The request took too long. Check your connection.');
        } else {
          Alert.alert('Error', error.message);
        }
    }
  }
}

Debug Mode

const { data } = await api.get('/users', {
  debug: true
});
// [Rezo Debug] GET https://api.myapp.com/v1/users
// [Rezo Debug] Adapter: react-native
// [Rezo Debug] Response: 200 OK (203.87ms)

Limitations

FeatureStatusReason
Proxy supportNot availableReact Native does not expose proxy configuration
Response streamingOptionalConfigure reactNative.streamTransport with a readable transport such as expo/fetch
HTTP/2 configurationNot availableHandled by the native networking layer
Cookie jarPartialRezo manages request/response cookies, but RN does not expose a native Node-style cookie jar
TLS configurationNot availableManaged by iOS/Android native TLS
Upload progressOptionalAvailable through configured file-system upload providers
Direct file I/OOptionalConfigure expo-file-system or react-native-fs through reactNative.fileSystemAdapter

Provider Setup

For file downloads in Expo:

import rezo, { createExpoFileSystemAdapter } from 'rezo/platform/react-native';
import * as ExpoFileSystem from 'expo-file-system';

const api = rezo.create({
  reactNative: {
    fileSystemAdapter: createExpoFileSystemAdapter(ExpoFileSystem)
  }
});

For Expo upload-task-based file uploads, pass the legacy upload task module explicitly:

import rezo, { createExpoFileSystemAdapter } from 'rezo/platform/react-native';
import * as ExpoFileSystem from 'expo-file-system';
import * as ExpoFileSystemLegacy from 'expo-file-system/legacy';

const api = rezo.create({
  reactNative: {
    fileSystemAdapter: createExpoFileSystemAdapter(ExpoFileSystem, {
      uploadTaskModule: ExpoFileSystemLegacy
    })
  }
});

Network State Awareness

When @react-native-community/netinfo is installed, wire it through createNetInfoProvider(...) and Rezo will run optional offline-aware preflight without making NetInfo a hard dependency:

import rezo, { createNetInfoProvider } from 'rezo/platform/react-native';
import NetInfo from '@react-native-community/netinfo';

const api = rezo.create({
  reactNative: {
    networkInfoProvider: createNetInfoProvider(NetInfo)
  }
});

When to Use This Adapter

The React Native adapter is the right choice when:

  • You are building a React Native or Expo application
  • You need native FormData handling with file URIs
  • You want Blob support for binary data
  • You need automatic retry for unreliable mobile networks
  • You want consistent Rezo API usage between your mobile and server code

For server-side Node.js APIs that your mobile app talks to, use the HTTP adapter. For web apps (React for web), use the Fetch adapter.