Skip to main content

Data & Networking

This guide covers API integration, data fetching patterns, offline capabilities, and real-time data management in React Native applications.

HTTP Client Setup

Using Fetch API

// services/api/client.ts
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || "https://api.example.com"

export class APIError extends Error {
constructor(message: string, public status: number, public response?: any) {
super(message)
this.name = "APIError"
}
}

export interface APIResponse<T = any> {
data: T
message: string
success: boolean
errors?: string[]
}

class APIClient {
private baseURL: string
private defaultHeaders: Record<string, string>

constructor(baseURL: string = API_BASE_URL) {
this.baseURL = baseURL
this.defaultHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
}
}

private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseURL}${endpoint}`

const config: RequestInit = {
...options,
headers: {
...this.defaultHeaders,
...options.headers,
},
}

try {
const response = await fetch(url, config)

if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new APIError(errorData.message || `HTTP ${response.status}`, response.status, errorData)
}

return await response.json()
} catch (error) {
if (error instanceof APIError) {
throw error
}
throw new APIError("Network request failed", 0)
}
}

async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const searchParams = params ? new URLSearchParams(params).toString() : ""
const url = searchParams ? `${endpoint}?${searchParams}` : endpoint

return this.request<T>(url, { method: "GET" })
}

async post<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "POST",
body: JSON.stringify(data),
})
}

async put<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "PUT",
body: JSON.stringify(data),
})
}

async patch<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>(endpoint, {
method: "PATCH",
body: JSON.stringify(data),
})
}

async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: "DELETE" })
}

setAuthToken(token: string) {
this.defaultHeaders["Authorization"] = `Bearer ${token}`
}

removeAuthToken() {
delete this.defaultHeaders["Authorization"]
}
}

export const apiClient = new APIClient()

Using Axios Alternative

// services/api/axios-client.ts
import axios, { AxiosInstance, AxiosResponse, AxiosError } from "axios"
import AsyncStorage from "@react-native-async-storage/async-storage"

class AxiosAPIClient {
private client: AxiosInstance

constructor() {
this.client = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
})

this.setupInterceptors()
}

private setupInterceptors() {
// Request interceptor for auth token
this.client.interceptors.request.use(
async (config) => {
const token = await AsyncStorage.getItem("auth_token")
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)

// Response interceptor for error handling
this.client.interceptors.response.use(
(response: AxiosResponse) => response.data,
async (error: AxiosError) => {
if (error.response?.status === 401) {
// Handle token expiration
await AsyncStorage.removeItem("auth_token")
// Redirect to login or refresh token
}
return Promise.reject(error)
}
)
}

async get<T>(url: string, params?: any): Promise<T> {
return this.client.get(url, { params })
}

async post<T>(url: string, data?: any): Promise<T> {
return this.client.post(url, data)
}

async put<T>(url: string, data?: any): Promise<T> {
return this.client.put(url, data)
}

async delete<T>(url: string): Promise<T> {
return this.client.delete(url)
}
}

export const axiosClient = new AxiosAPIClient()

API Service Layer

// services/api/auth.ts
export interface LoginRequest {
email: string
password: string
}

export interface LoginResponse {
user: User
token: string
refreshToken: string
}

export interface User {
id: string
email: string
name: string
avatar?: string
}

export class AuthService {
static async login(credentials: LoginRequest): Promise<LoginResponse> {
return apiClient.post<LoginResponse>("/auth/login", credentials)
}

static async register(userData: RegisterRequest): Promise<LoginResponse> {
return apiClient.post<LoginResponse>("/auth/register", userData)
}

static async refreshToken(refreshToken: string): Promise<{ token: string }> {
return apiClient.post<{ token: string }>("/auth/refresh", { refreshToken })
}

static async logout(): Promise<void> {
return apiClient.post("/auth/logout")
}

static async getProfile(): Promise<User> {
return apiClient.get<User>("/auth/profile")
}

static async updateProfile(data: Partial<User>): Promise<User> {
return apiClient.patch<User>("/auth/profile", data)
}

static async changePassword(data: { currentPassword: string; newPassword: string }): Promise<void> {
return apiClient.post("/auth/change-password", data)
}

static async resetPassword(email: string): Promise<void> {
return apiClient.post("/auth/reset-password", { email })
}
}

// services/api/users.ts
export interface UsersListResponse {
users: User[]
pagination: {
page: number
limit: number
total: number
hasNext: boolean
}
}

export class UsersService {
static async getUsers(params?: { page?: number; limit?: number; search?: string }): Promise<UsersListResponse> {
return apiClient.get<UsersListResponse>("/users", params)
}

static async getUser(id: string): Promise<User> {
return apiClient.get<User>(`/users/${id}`)
}

static async createUser(userData: Omit<User, "id">): Promise<User> {
return apiClient.post<User>("/users", userData)
}

static async updateUser(id: string, data: Partial<User>): Promise<User> {
return apiClient.patch<User>(`/users/${id}`, data)
}

static async deleteUser(id: string): Promise<void> {
return apiClient.delete(`/users/${id}`)
}
}

React Query Integration

Setup and Configuration

// hooks/query-client.ts
import { QueryClient } from "@tanstack/react-query"
import { focusManager } from "@tanstack/react-query"
import { AppState, Platform } from "react-native"
import NetInfo from "@react-native-netinfo/netinfo"

// Configure focus manager for mobile
function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== "web") {
focusManager.setFocused(status === "active")
}
}

AppState.addEventListener("change", onAppStateChange)

// Configure online manager
NetInfo.configure({
reachabilityUrl: "https://clients3.google.com/generate_204",
reachabilityTest: async (response) => response.status === 204,
reachabilityLongTimeout: 60 * 1000,
reachabilityShortTimeout: 5 * 1000,
reachabilityRequestTimeout: 15 * 1000,
})

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (error.status === 404) return false
return failureCount < 3
},
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
},
mutations: {
retry: 1,
},
},
})

Data Fetching Hooks

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { UsersService, User } from "@/services/api/users"

export function useUsers(params?: { page?: number; search?: string }) {
return useQuery({
queryKey: ["users", params],
queryFn: () => UsersService.getUsers(params),
keepPreviousData: true,
})
}

export function useUser(id: string) {
return useQuery({
queryKey: ["user", id],
queryFn: () => UsersService.getUser(id),
enabled: !!id,
})
}

export function useCreateUser() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: UsersService.createUser,
onSuccess: (newUser) => {
// Update the users list cache
queryClient.setQueryData(["users"], (old: any) => {
if (!old) return old
return {
...old,
users: [newUser, ...old.users],
}
})
},
})
}

export function useUpdateUser() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) => UsersService.updateUser(id, data),
onSuccess: (updatedUser) => {
// Update individual user cache
queryClient.setQueryData(["user", updatedUser.id], updatedUser)

// Update users list cache
queryClient.setQueryData(["users"], (old: any) => {
if (!old) return old
return {
...old,
users: old.users.map((user: User) => (user.id === updatedUser.id ? updatedUser : user)),
}
})
},
})
}

export function useDeleteUser() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: UsersService.deleteUser,
onSuccess: (_, deletedId) => {
// Remove from cache
queryClient.removeQueries(["user", deletedId])

// Update users list
queryClient.setQueryData(["users"], (old: any) => {
if (!old) return old
return {
...old,
users: old.users.filter((user: User) => user.id !== deletedId),
}
})
},
})
}

Authentication Hooks

// hooks/useAuth.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { AuthService } from "@/services/api/auth"
import AsyncStorage from "@react-native-async-storage/async-storage"

export function useProfile() {
return useQuery({
queryKey: ["profile"],
queryFn: AuthService.getProfile,
retry: false,
})
}

export function useLogin() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: AuthService.login,
onSuccess: async (data) => {
// Store token
await AsyncStorage.setItem("auth_token", data.token)
await AsyncStorage.setItem("refresh_token", data.refreshToken)

// Set user in cache
queryClient.setQueryData(["profile"], data.user)

// Invalidate and refetch protected queries
queryClient.invalidateQueries()
},
})
}

export function useLogout() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: AuthService.logout,
onSuccess: async () => {
// Clear tokens
await AsyncStorage.removeItem("auth_token")
await AsyncStorage.removeItem("refresh_token")

// Clear all cache
queryClient.clear()
},
})
}

export function useUpdateProfile() {
const queryClient = useQueryClient()

return useMutation({
mutationFn: AuthService.updateProfile,
onSuccess: (updatedUser) => {
queryClient.setQueryData(["profile"], updatedUser)
},
})
}

Offline Support

Network Status Detection

// hooks/useNetworkStatus.ts
import { useState, useEffect } from "react"
import NetInfo from "@react-native-netinfo/netinfo"

export function useNetworkStatus() {
const [isConnected, setIsConnected] = useState<boolean | null>(null)
const [connectionType, setConnectionType] = useState<string | null>(null)

useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
setIsConnected(state.isConnected)
setConnectionType(state.type)
})

return () => unsubscribe()
}, [])

return { isConnected, connectionType }
}

Offline Storage with SQLite

// services/database/database.ts
import * as SQLite from "expo-sqlite"

export interface OfflineAction {
id: string
type: "CREATE" | "UPDATE" | "DELETE"
endpoint: string
data: any
timestamp: number
}

class OfflineDatabase {
private db: SQLite.Database

constructor() {
this.db = SQLite.openDatabase("offline.db")
this.initializeTables()
}

private initializeTables() {
this.db.transaction((tx) => {
tx.executeSql(`
CREATE TABLE IF NOT EXISTS offline_actions (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
endpoint TEXT NOT NULL,
data TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
`)

tx.executeSql(`
CREATE TABLE IF NOT EXISTS cached_data (
key TEXT PRIMARY KEY,
data TEXT NOT NULL,
timestamp INTEGER NOT NULL,
expiry INTEGER
);
`)
})
}

async addOfflineAction(action: OfflineAction): Promise<void> {
return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"INSERT INTO offline_actions (id, type, endpoint, data, timestamp) VALUES (?, ?, ?, ?, ?)",
[action.id, action.type, action.endpoint, JSON.stringify(action.data), action.timestamp],
() => resolve(),
(_, error) => {
reject(error)
return false
}
)
})
})
}

async getOfflineActions(): Promise<OfflineAction[]> {
return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"SELECT * FROM offline_actions ORDER BY timestamp ASC",
[],
(_, { rows }) => {
const actions = rows._array.map((row) => ({
...row,
data: JSON.parse(row.data),
}))
resolve(actions)
},
(_, error) => {
reject(error)
return false
}
)
})
})
}

async removeOfflineAction(id: string): Promise<void> {
return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"DELETE FROM offline_actions WHERE id = ?",
[id],
() => resolve(),
(_, error) => {
reject(error)
return false
}
)
})
})
}

async cacheData(key: string, data: any, expiryMinutes?: number): Promise<void> {
const timestamp = Date.now()
const expiry = expiryMinutes ? timestamp + expiryMinutes * 60 * 1000 : null

return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"INSERT OR REPLACE INTO cached_data (key, data, timestamp, expiry) VALUES (?, ?, ?, ?)",
[key, JSON.stringify(data), timestamp, expiry],
() => resolve(),
(_, error) => {
reject(error)
return false
}
)
})
})
}

async getCachedData(key: string): Promise<any | null> {
return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"SELECT * FROM cached_data WHERE key = ?",
[key],
(_, { rows }) => {
if (rows.length === 0) {
resolve(null)
return
}

const row = rows._array[0]

// Check if expired
if (row.expiry && Date.now() > row.expiry) {
this.removeCachedData(key)
resolve(null)
return
}

resolve(JSON.parse(row.data))
},
(_, error) => {
reject(error)
return false
}
)
})
})
}

async removeCachedData(key: string): Promise<void> {
return new Promise((resolve, reject) => {
this.db.transaction((tx) => {
tx.executeSql(
"DELETE FROM cached_data WHERE key = ?",
[key],
() => resolve(),
(_, error) => {
reject(error)
return false
}
)
})
})
}
}

export const offlineDB = new OfflineDatabase()

Offline-First Data Sync

// services/offline/sync-manager.ts
import { offlineDB } from "../database/database"
import { apiClient } from "../api/client"
import { useNetworkStatus } from "@/hooks/useNetworkStatus"

class SyncManager {
private isRunning = false

async syncOfflineActions(): Promise<void> {
if (this.isRunning) return

this.isRunning = true

try {
const actions = await offlineDB.getOfflineActions()

for (const action of actions) {
try {
await this.executeAction(action)
await offlineDB.removeOfflineAction(action.id)
} catch (error) {
console.error("Failed to sync action:", action, error)
// Keep action for retry
}
}
} finally {
this.isRunning = false
}
}

private async executeAction(action: OfflineAction): Promise<void> {
const { type, endpoint, data } = action

switch (type) {
case "CREATE":
await apiClient.post(endpoint, data)
break
case "UPDATE":
await apiClient.patch(endpoint, data)
break
case "DELETE":
await apiClient.delete(endpoint)
break
}
}

async queueOfflineAction(type: OfflineAction["type"], endpoint: string, data: any): Promise<void> {
const action: OfflineAction = {
id: `${type}_${endpoint}_${Date.now()}`,
type,
endpoint,
data,
timestamp: Date.now(),
}

await offlineDB.addOfflineAction(action)
}
}

export const syncManager = new SyncManager()

// Hook to automatically sync when online
export function useOfflineSync() {
const { isConnected } = useNetworkStatus()

useEffect(() => {
if (isConnected) {
syncManager.syncOfflineActions()
}
}, [isConnected])

return { sync: () => syncManager.syncOfflineActions() }
}

Real-Time Data with WebSockets

// services/websocket/websocket-client.ts
import { useEffect, useRef, useState } from "react"

export type WebSocketMessage = {
type: string
payload: any
timestamp: number
}

export type ConnectionStatus = "connecting" | "connected" | "disconnected" | "error"

class WebSocketClient {
private ws: WebSocket | null = null
private url: string
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 1000
private listeners: Map<string, Set<(data: any) => void>> = new Map()
private statusListeners: Set<(status: ConnectionStatus) => void> = new Set()

constructor(url: string) {
this.url = url
}

connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) return

this.notifyStatusChange("connecting")
this.ws = new WebSocket(this.url)

this.ws.onopen = () => {
this.reconnectAttempts = 0
this.notifyStatusChange("connected")
}

this.ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data)
this.handleMessage(message)
} catch (error) {
console.error("Failed to parse WebSocket message:", error)
}
}

this.ws.onclose = () => {
this.notifyStatusChange("disconnected")
this.scheduleReconnect()
}

this.ws.onerror = () => {
this.notifyStatusChange("error")
}
}

disconnect(): void {
if (this.ws) {
this.ws.close()
this.ws = null
}
}

send(message: WebSocketMessage): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message))
}
}

subscribe(type: string, callback: (data: any) => void): () => void {
if (!this.listeners.has(type)) {
this.listeners.set(type, new Set())
}
this.listeners.get(type)!.add(callback)

return () => {
const callbacks = this.listeners.get(type)
if (callbacks) {
callbacks.delete(callback)
if (callbacks.size === 0) {
this.listeners.delete(type)
}
}
}
}

onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
this.statusListeners.add(callback)
return () => this.statusListeners.delete(callback)
}

private handleMessage(message: WebSocketMessage): void {
const callbacks = this.listeners.get(message.type)
if (callbacks) {
callbacks.forEach((callback) => callback(message.payload))
}
}

private notifyStatusChange(status: ConnectionStatus): void {
this.statusListeners.forEach((callback) => callback(status))
}

private scheduleReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(() => {
this.reconnectAttempts++
this.connect()
}, this.reconnectInterval * Math.pow(2, this.reconnectAttempts))
}
}
}

// React hook for WebSocket connection
export function useWebSocket(url: string) {
const wsRef = useRef<WebSocketClient | null>(null)
const [status, setStatus] = useState<ConnectionStatus>("disconnected")

useEffect(() => {
wsRef.current = new WebSocketClient(url)

const unsubscribeStatus = wsRef.current.onStatusChange(setStatus)
wsRef.current.connect()

return () => {
unsubscribeStatus()
wsRef.current?.disconnect()
}
}, [url])

const send = (message: WebSocketMessage) => {
wsRef.current?.send(message)
}

const subscribe = (type: string, callback: (data: any) => void) => {
return wsRef.current?.subscribe(type, callback) || (() => {})
}

return { send, subscribe, status }
}

// Example usage
function ChatScreen() {
const { send, subscribe, status } = useWebSocket("wss://api.example.com/chat")
const [messages, setMessages] = useState<any[]>([])

useEffect(() => {
const unsubscribe = subscribe("message", (message) => {
setMessages((prev) => [...prev, message])
})

return unsubscribe
}, [subscribe])

const sendMessage = (text: string) => {
send({
type: "message",
payload: { text, userId: "current-user-id" },
timestamp: Date.now(),
})
}

return (
<View>
<Text>Status: {status}</Text>
<FlatList
data={messages}
renderItem={({ item }) => <Text>{item.text}</Text>}
keyExtractor={(item, index) => index.toString()}
/>
</View>
)
}

File Upload and Download

// services/upload/file-upload.ts
import * as DocumentPicker from "expo-document-picker"
import * as ImagePicker from "expo-image-picker"

export interface UploadProgress {
loaded: number
total: number
percentage: number
}

export class FileUploadService {
static async uploadFile(
file: File | string,
endpoint: string,
onProgress?: (progress: UploadProgress) => void
): Promise<any> {
const formData = new FormData()

if (typeof file === "string") {
// File URI from device
formData.append("file", {
uri: file,
type: "image/jpeg", // Detect actual type
name: file.split("/").pop() || "file",
} as any)
} else {
formData.append("file", file)
}

return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()

xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable && onProgress) {
onProgress({
loaded: event.loaded,
total: event.total,
percentage: (event.loaded / event.total) * 100,
})
}
})

xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText))
} else {
reject(new Error(`Upload failed: ${xhr.status}`))
}
})

xhr.addEventListener("error", () => {
reject(new Error("Upload failed"))
})

xhr.open("POST", endpoint)
xhr.setRequestHeader("Authorization", "Bearer " + getAuthToken())
xhr.send(formData)
})
}

static async pickImage(): Promise<string | null> {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()

if (status !== "granted") {
throw new Error("Permission denied")
}

const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
})

return result.canceled ? null : result.assets[0].uri
}

static async pickDocument(): Promise<string | null> {
const result = await DocumentPicker.getDocumentAsync({
type: "*/*",
copyToCacheDirectory: true,
})

return result.canceled ? null : result.assets[0].uri
}
}

// Hook for file uploads
export function useFileUpload() {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState<UploadProgress | null>(null)

const upload = async (file: string, endpoint: string) => {
setUploading(true)
setProgress(null)

try {
const result = await FileUploadService.uploadFile(file, endpoint, setProgress)
return result
} finally {
setUploading(false)
setProgress(null)
}
}

return { upload, uploading, progress }
}

Next Steps

Continue with these related topics: