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
- Basic Fetch Usage
Simple GET request:
const fetchUser = async (userId: string) => {
const response = await fetch(https://api.example.com/users/${userId});
if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}
return response.json(); };
POST request with body:
const createUser = async (userData: UserData) => {
const response = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: Bearer ${token},
},
body: JSON.stringify(userData),
});
if (!response.ok) { const error = await response.json(); throw new Error(error.message); }
return response.json(); };
- React Query (TanStack Query)
Setup:
// app/_layout.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 minutes retry: 2, }, }, });
export default function RootLayout() { return ( <QueryClientProvider client={queryClient}> <Stack /> </QueryClientProvider> ); }
Fetching data:
import { useQuery } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: string }) { const { data, isLoading, error, refetch } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId), });
if (isLoading) return <Loading />; if (error) return <Error message={error.message} />;
return <Profile user={data} />; }
Mutations:
import { useMutation, useQueryClient } from "@tanstack/react-query";
function CreateUserForm() { const queryClient = useQueryClient();
const mutation = useMutation({ mutationFn: createUser, onSuccess: () => { // Invalidate and refetch queryClient.invalidateQueries({ queryKey: ["users"] }); }, });
const handleSubmit = (data: UserData) => { mutation.mutate(data); };
return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} />; }
- Error Handling
Comprehensive error handling:
class ApiError extends Error { constructor(message: string, public status: number, public code?: string) { super(message); this.name = "ApiError"; } }
const fetchWithErrorHandling = async (url: string, options?: RequestInit) => { try { const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(
error.message || "Request failed",
response.status,
error.code
);
}
return response.json();
} catch (error) { if (error instanceof ApiError) { throw error; } // Network error (no internet, timeout, etc.) throw new ApiError("Network error", 0, "NETWORK_ERROR"); } };
Retry logic:
const fetchWithRetry = async ( url: string, options?: RequestInit, retries = 3 ) => { for (let i = 0; i < retries; i++) { try { return await fetchWithErrorHandling(url, options); } catch (error) { if (i === retries - 1) throw error; // Exponential backoff await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000)); } } };
- Authentication
Token management:
import * as SecureStore from "expo-secure-store";
const TOKEN_KEY = "auth_token";
export const auth = { getToken: () => SecureStore.getItemAsync(TOKEN_KEY), setToken: (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token), removeToken: () => SecureStore.deleteItemAsync(TOKEN_KEY), };
// Authenticated fetch wrapper const authFetch = async (url: string, options: RequestInit = {}) => { const token = await auth.getToken();
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: token ? Bearer ${token} : "",
},
});
};
Token refresh:
let isRefreshing = false; let refreshPromise: Promise<string> | null = null;
const getValidToken = async (): Promise<string> => { const token = await auth.getToken();
if (!token || isTokenExpired(token)) { if (!isRefreshing) { isRefreshing = true; refreshPromise = refreshToken().finally(() => { isRefreshing = false; refreshPromise = null; }); } return refreshPromise!; }
return token; };
- Offline Support
Check network status:
import NetInfo from "@react-native-community/netinfo";
// Hook for network status function useNetworkStatus() { const [isOnline, setIsOnline] = useState(true);
useEffect(() => { return NetInfo.addEventListener((state) => { setIsOnline(state.isConnected ?? true); }); }, []);
return isOnline; }
Offline-first with React Query:
import { onlineManager } from "@tanstack/react-query"; import NetInfo from "@react-native-community/netinfo";
// Sync React Query with network status onlineManager.setEventListener((setOnline) => { return NetInfo.addEventListener((state) => { setOnline(state.isConnected ?? true); }); });
// Queries will pause when offline and resume when online
- 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.
// .env EXPO_PUBLIC_API_URL=https://api.example.com EXPO_PUBLIC_API_VERSION=v1
// Usage in code const API_URL = process.env.EXPO_PUBLIC_API_URL;
const fetchUsers = async () => {
const response = await fetch(${API_URL}/users);
return response.json();
};
Environment-specific configuration:
// .env.development EXPO_PUBLIC_API_URL=http://localhost:3000
// .env.production EXPO_PUBLIC_API_URL=https://api.production.com
Creating an API client with environment config:
// api/client.ts const BASE_URL = process.env.EXPO_PUBLIC_API_URL;
if (!BASE_URL) { throw new Error("EXPO_PUBLIC_API_URL is not defined"); }
export const apiClient = {
get: async <T,>(path: string): Promise<T> => {
const response = await fetch(${BASE_URL}${path});
if (!response.ok) throw new Error(HTTP ${response.status});
return response.json();
},
post: async <T,>(path: string, body: unknown): Promise<T> => {
const response = await fetch(${BASE_URL}${path}, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(HTTP ${response.status});
return response.json();
},
};
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 .env files
-
For server-side secrets in API routes, use variables without the EXPO_PUBLIC_ prefix
TypeScript support:
// types/env.d.ts declare global { namespace NodeJS { interface ProcessEnv { EXPO_PUBLIC_API_URL: string; EXPO_PUBLIC_API_VERSION?: string; } } }
export {};
- Request Cancellation
Cancel on unmount:
useEffect(() => { const controller = new AbortController();
fetch(url, { signal: controller.signal }) .then((response) => response.json()) .then(setData) .catch((error) => { if (error.name !== "AbortError") { setError(error); } });
return () => controller.abort(); }, [url]);
With React Query (automatic):
// React Query automatically cancels requests when queries are invalidated // 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
const data = await fetch(url).then((r) => r.json());
Right: Check response status
const response = await fetch(url);
if (!response.ok) throw new Error(HTTP ${response.status});
const data = await response.json();
Wrong: Storing tokens in AsyncStorage
await AsyncStorage.setItem("token", token); // Not secure!
Right: Use SecureStore for sensitive data
await 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