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
| Feature | Status | Reason |
|---|---|---|
| Proxy support | Not available | React Native does not expose proxy configuration |
| Response streaming | Optional | Configure reactNative.streamTransport with a readable transport such as expo/fetch |
| HTTP/2 configuration | Not available | Handled by the native networking layer |
| Cookie jar | Partial | Rezo manages request/response cookies, but RN does not expose a native Node-style cookie jar |
| TLS configuration | Not available | Managed by iOS/Android native TLS |
| Upload progress | Optional | Available through configured file-system upload providers |
| Direct file I/O | Optional | Configure 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.