Expo Networking
You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.
When to Use
Use this router when:
- Implementing API requests
- Setting up data fetching (React Query, SWR)
- Debugging network failures
- Implementing caching strategies
- Handling offline scenarios
- Authentication/token management
- Configuring API URLs and environment variables
Preferences
- Avoid axios, prefer expo/fetch
Common Issues & Solutions
1. Basic Fetch Usage
Simple GET request:
tsx1const fetchUser = async (userId: string) => { 2 const response = await fetch(`https://api.example.com/users/${userId}`); 3 4 if (!response.ok) { 5 throw new Error(`HTTP error! status: ${response.status}`); 6 } 7 8 return response.json(); 9};
POST request with body:
tsx1const createUser = async (userData: UserData) => { 2 const response = await fetch("https://api.example.com/users", { 3 method: "POST", 4 headers: { 5 "Content-Type": "application/json", 6 Authorization: `Bearer ${token}`, 7 }, 8 body: JSON.stringify(userData), 9 }); 10 11 if (!response.ok) { 12 const error = await response.json(); 13 throw new Error(error.message); 14 } 15 16 return response.json(); 17};
2. React Query (TanStack Query)
Setup:
tsx1// app/_layout.tsx 2import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 4const queryClient = new QueryClient({ 5 defaultOptions: { 6 queries: { 7 staleTime: 1000 * 60 * 5, // 5 minutes 8 retry: 2, 9 }, 10 }, 11}); 12 13export default function RootLayout() { 14 return ( 15 <QueryClientProvider client={queryClient}> 16 <Stack /> 17 </QueryClientProvider> 18 ); 19}
Fetching data:
tsx1import { useQuery } from "@tanstack/react-query"; 2 3function UserProfile({ userId }: { userId: string }) { 4 const { data, isLoading, error, refetch } = useQuery({ 5 queryKey: ["user", userId], 6 queryFn: () => fetchUser(userId), 7 }); 8 9 if (isLoading) return <Loading />; 10 if (error) return <Error message={error.message} />; 11 12 return <Profile user={data} />; 13}
Mutations:
tsx1import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 3function CreateUserForm() { 4 const queryClient = useQueryClient(); 5 6 const mutation = useMutation({ 7 mutationFn: createUser, 8 onSuccess: () => { 9 // Invalidate and refetch 10 queryClient.invalidateQueries({ queryKey: ["users"] }); 11 }, 12 }); 13 14 const handleSubmit = (data: UserData) => { 15 mutation.mutate(data); 16 }; 17 18 return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} />; 19}
3. Error Handling
Comprehensive error handling:
tsx1class ApiError extends Error { 2 constructor(message: string, public status: number, public code?: string) { 3 super(message); 4 this.name = "ApiError"; 5 } 6} 7 8const fetchWithErrorHandling = async (url: string, options?: RequestInit) => { 9 try { 10 const response = await fetch(url, options); 11 12 if (!response.ok) { 13 const error = await response.json().catch(() => ({})); 14 throw new ApiError( 15 error.message || "Request failed", 16 response.status, 17 error.code 18 ); 19 } 20 21 return response.json(); 22 } catch (error) { 23 if (error instanceof ApiError) { 24 throw error; 25 } 26 // Network error (no internet, timeout, etc.) 27 throw new ApiError("Network error", 0, "NETWORK_ERROR"); 28 } 29};
Retry logic:
tsx1const fetchWithRetry = async ( 2 url: string, 3 options?: RequestInit, 4 retries = 3 5) => { 6 for (let i = 0; i < retries; i++) { 7 try { 8 return await fetchWithErrorHandling(url, options); 9 } catch (error) { 10 if (i === retries - 1) throw error; 11 // Exponential backoff 12 await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000)); 13 } 14 } 15};
4. Authentication
Token management:
tsx1import * as SecureStore from "expo-secure-store"; 2 3const TOKEN_KEY = "auth_token"; 4 5export const auth = { 6 getToken: () => SecureStore.getItemAsync(TOKEN_KEY), 7 setToken: (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token), 8 removeToken: () => SecureStore.deleteItemAsync(TOKEN_KEY), 9}; 10 11// Authenticated fetch wrapper 12const authFetch = async (url: string, options: RequestInit = {}) => { 13 const token = await auth.getToken(); 14 15 return fetch(url, { 16 ...options, 17 headers: { 18 ...options.headers, 19 Authorization: token ? `Bearer ${token}` : "", 20 }, 21 }); 22};
Token refresh:
tsx1let isRefreshing = false; 2let refreshPromise: Promise<string> | null = null; 3 4const getValidToken = async (): Promise<string> => { 5 const token = await auth.getToken(); 6 7 if (!token || isTokenExpired(token)) { 8 if (!isRefreshing) { 9 isRefreshing = true; 10 refreshPromise = refreshToken().finally(() => { 11 isRefreshing = false; 12 refreshPromise = null; 13 }); 14 } 15 return refreshPromise!; 16 } 17 18 return token; 19};
5. Offline Support
Check network status:
tsx1import NetInfo from "@react-native-community/netinfo"; 2 3// Hook for network status 4function useNetworkStatus() { 5 const [isOnline, setIsOnline] = useState(true); 6 7 useEffect(() => { 8 return NetInfo.addEventListener((state) => { 9 setIsOnline(state.isConnected ?? true); 10 }); 11 }, []); 12 13 return isOnline; 14}
Offline-first with React Query:
tsx1import { onlineManager } from "@tanstack/react-query"; 2import NetInfo from "@react-native-community/netinfo"; 3 4// Sync React Query with network status 5onlineManager.setEventListener((setOnline) => { 6 return NetInfo.addEventListener((state) => { 7 setOnline(state.isConnected ?? true); 8 }); 9}); 10 11// Queries will pause when offline and resume when online
6. Environment Variables
Using environment variables for API configuration:
Expo supports environment variables with the EXPO_PUBLIC_ prefix. These are inlined at build time and available in your JavaScript code.
tsx1// .env 2EXPO_PUBLIC_API_URL=https://api.example.com 3EXPO_PUBLIC_API_VERSION=v1 4 5// Usage in code 6const API_URL = process.env.EXPO_PUBLIC_API_URL; 7 8const fetchUsers = async () => { 9 const response = await fetch(`${API_URL}/users`); 10 return response.json(); 11};
Environment-specific configuration:
tsx1// .env.development 2EXPO_PUBLIC_API_URL=http://localhost:3000 3 4// .env.production 5EXPO_PUBLIC_API_URL=https://api.production.com
Creating an API client with environment config:
tsx1// api/client.ts 2const BASE_URL = process.env.EXPO_PUBLIC_API_URL; 3 4if (!BASE_URL) { 5 throw new Error("EXPO_PUBLIC_API_URL is not defined"); 6} 7 8export const apiClient = { 9 get: async <T,>(path: string): Promise<T> => { 10 const response = await fetch(`${BASE_URL}${path}`); 11 if (!response.ok) throw new Error(`HTTP ${response.status}`); 12 return response.json(); 13 }, 14 15 post: async <T,>(path: string, body: unknown): Promise<T> => { 16 const response = await fetch(`${BASE_URL}${path}`, { 17 method: "POST", 18 headers: { "Content-Type": "application/json" }, 19 body: JSON.stringify(body), 20 }); 21 if (!response.ok) throw new Error(`HTTP ${response.status}`); 22 return response.json(); 23 }, 24};
Important notes:
- Only variables prefixed with
EXPO_PUBLIC_are exposed to the client bundle - Never put secrets (API keys with write access, database passwords) in
EXPO_PUBLIC_variables—they're visible in the built app - Environment variables are inlined at build time, not runtime
- Restart the dev server after changing
.envfiles - For server-side secrets in API routes, use variables without the
EXPO_PUBLIC_prefix
TypeScript support:
tsx1// types/env.d.ts 2declare global { 3 namespace NodeJS { 4 interface ProcessEnv { 5 EXPO_PUBLIC_API_URL: string; 6 EXPO_PUBLIC_API_VERSION?: string; 7 } 8 } 9} 10 11export {};
7. Request Cancellation
Cancel on unmount:
tsx1useEffect(() => { 2 const controller = new AbortController(); 3 4 fetch(url, { signal: controller.signal }) 5 .then((response) => response.json()) 6 .then(setData) 7 .catch((error) => { 8 if (error.name !== "AbortError") { 9 setError(error); 10 } 11 }); 12 13 return () => controller.abort(); 14}, [url]);
With React Query (automatic):
tsx1// React Query automatically cancels requests when queries are invalidated 2// or components unmount
Decision Tree
User asks about networking
|-- Basic fetch?
| \-- Use fetch API with error handling
|
|-- Need caching/state management?
| |-- Complex app -> React Query (TanStack Query)
| \-- Simpler needs -> SWR or custom hooks
|
|-- Authentication?
| |-- Token storage -> expo-secure-store
| \-- Token refresh -> Implement refresh flow
|
|-- Error handling?
| |-- Network errors -> Check connectivity first
| |-- HTTP errors -> Parse response, throw typed errors
| \-- Retries -> Exponential backoff
|
|-- Offline support?
| |-- Check status -> NetInfo
| \-- Queue requests -> React Query persistence
|
|-- Environment/API config?
| |-- Client-side URLs -> EXPO_PUBLIC_ prefix in .env
| |-- Server secrets -> Non-prefixed env vars (API routes only)
| \-- Multiple environments -> .env.development, .env.production
|
\-- Performance?
|-- Caching -> React Query with staleTime
|-- Deduplication -> React Query handles this
\-- Cancellation -> AbortController or React Query
Common Mistakes
Wrong: No error handling
tsx1const data = await fetch(url).then((r) => r.json());
Right: Check response status
tsx1const response = await fetch(url); 2if (!response.ok) throw new Error(`HTTP ${response.status}`); 3const data = await response.json();
Wrong: Storing tokens in AsyncStorage
tsx1await AsyncStorage.setItem("token", token); // Not secure!
Right: Use SecureStore for sensitive data
tsx1await SecureStore.setItemAsync("token", token);
Example Invocations
User: "How do I make API calls in React Native?" -> Use fetch, wrap with error handling
User: "Should I use React Query or SWR?" -> React Query for complex apps, SWR for simpler needs
User: "My app needs to work offline" -> Use NetInfo for status, React Query persistence for caching
User: "How do I handle authentication tokens?" -> Store in expo-secure-store, implement refresh flow
User: "API calls are slow" -> Check caching strategy, use React Query staleTime
User: "How do I configure different API URLs for dev and prod?" -> Use EXPOPUBLIC env vars with .env.development and .env.production files
User: "Where should I put my API key?" -> Client-safe keys: EXPOPUBLIC in .env. Secret keys: non-prefixed env vars in API routes only