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:
tsx
1const 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:
tsx
1const 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:
tsx
1// 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:
tsx
1import { 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:
tsx
1import { 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:
tsx
1class 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:
tsx
1const 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:
tsx
1import * 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:
tsx
1let 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:
tsx
1import 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:
tsx
1import { 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.
tsx
1// .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:
tsx
1// .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:
tsx
1// 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
.env files
- For server-side secrets in API routes, use variables without the
EXPO_PUBLIC_ prefix
TypeScript support:
tsx
1// 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:
tsx
1useEffect(() => {
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):
tsx
1// 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
tsx
1const data = await fetch(url).then((r) => r.json());
Right: Check response status
tsx
1const response = await fetch(url);
2if (!response.ok) throw new Error(`HTTP ${response.status}`);
3const data = await response.json();
Wrong: Storing tokens in AsyncStorage
tsx
1await AsyncStorage.setItem("token", token); // Not secure!
Right: Use SecureStore for sensitive data
tsx
1await 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