Adapters

React Native Adapter

The React Native adapter is optimized for mobile applications built with React Native and Expo. It uses the Fetch API (React Native’s built-in polyfill) with mobile-specific features like native Blob and FormData handling, Expo environment detection, and awareness of mobile network conditions.

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

Import and Setup

import rezo, {
  Rezo,
  RezoError,
  RezoHeaders,
  RezoFormData
} from 'rezo/adapters/react-native';

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

// Custom instance for your API
const api = new Rezo({
  baseURL: 'https://api.myapp.com/v1',
  timeout: 15000,
  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);

Download Progress

The adapter tracks download progress through response body consumption:

const api = new Rezo({
  hooks: {
    onDownloadProgress: [({ loaded, total, percent }) => {
      if (total) {
        setProgress(percent / 100);
      }
    }]
  }
});

const { data } = await api.get('/files/large-dataset.json');

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 = new Rezo({
      hooks: {
        onDownloadProgress: [({ percent }) => setProgress(percent)]
      }
    });

    try {
      const { data } = await client.get(url);
      // Handle downloaded data
    } 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 = new Rezo({
  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 = new Rezo({
  baseURL: 'https://api.myapp.com',
  cache: {
    maxAge: 300000,   // 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 = new Rezo({
  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 streamingLimitedReact Native buffers the entire response
HTTP/2 configurationNot availableHandled by the native networking layer
Cookie jarNot availableManual header management only
TLS configurationNot availableManaged by iOS/Android native TLS
Upload progressNot availableReact Native’s fetch does not expose upload events
Direct file I/ONot availableUse expo-file-system or react-native-fs

File I/O Workaround

Since React Native does not provide direct filesystem access, use expo-file-system or react-native-fs:

import * as FileSystem from 'expo-file-system';

// Download a file to the device
const downloadResult = await FileSystem.downloadAsync(
  'https://example.com/file.pdf',
  FileSystem.documentDirectory + 'file.pdf'
);

// For uploads, use file URIs with FormData (see File Uploads section above)

Network State Awareness

When @react-native-community/netinfo is installed, you can check network state before making requests:

import NetInfo from '@react-native-community/netinfo';

async function fetchWithNetworkCheck(url) {
  const state = await NetInfo.fetch();

  if (!state.isConnected) {
    throw new RezoError('No network connection', 'ERR_NETWORK');
  }

  if (state.type === 'cellular' && state.details?.cellularGeneration === '2g') {
    // Use longer timeout for slow connections
    return api.get(url, { timeout: 30000 });
  }

  return api.get(url, { timeout: 10000 });
}

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.