# Expo Networking - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. ## References Consult these resources as needed: ``` references/ expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+) ``` The snippets are separate integration examples: supply application types/components, React/Expo imports, verified API origins, schema parsers and an actual refresh adapter. A generic TypeScript return type does not validate JSON. Scope query keys to the authenticated tenant/user and clear private cached/persisted data on logout or identity changes. SecureStore requires a supported native platform; web needs a separately reviewed session strategy. Persistence does not make mutations a durable offline queue. ## Preferences - Avoid axios, prefer expo/fetch ## Loading, empty, failure and saved content Restore the separate screen states from the current [upstream fetching skill](https://github.com/expo/skills/blob/main/plugins/expo/skills/expo-data-fetching/SKILL.md): treat absent data separately from a resolved empty response, retain saved content during a failed refresh, and preserve an unsaved form after failed submission. Hydrate identity before private requests. These are application responsibilities, not states supplied by this guide. ## Common Issues & Solutions ### 1. Basic Fetch Usage **Simple GET request**: ```tsx const fetchUser = async (userId: string, signal?: AbortSignal) => { const response = await fetch(`https://api.example.com/users/${encodeURIComponent(userId)}`, { signal }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }; ``` **POST request with body**: ```tsx 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(); }; ``` --- ### 2. React Query (TanStack Query) **Setup**: ```tsx // 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 ( ); } ``` **Fetching data**: ```tsx import { useQuery } from "@tanstack/react-query"; function UserProfile({ userId }: { userId: string }) { const { data, fetchStatus, error, refetch } = useQuery({ queryKey: ["user", userId], queryFn: ({ signal }) => fetchUser(userId, signal), }); if (data === undefined) { if (error) return ; if (fetchStatus === "paused") return ; return ; } return <> {error && } {data === null ? : } ; } ``` **Mutations**: ```tsx 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) => { if (!mutation.isPending) mutation.mutate(data); }; return
; } ``` --- ### 3. Error Handling **Comprehensive error handling**: ```tsx 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; } if (options?.signal?.aborted || !(error instanceof TypeError)) throw error; // Fetch rejects network failures with TypeError; do not classify JSON parse errors as retryable. throw new ApiError("Network error", 0, "NETWORK_ERROR"); } }; ``` **Retry logic**: ```tsx const fetchWithRetry = async ( url: string, options?: RequestInit, retries = 3 ) => { if (!Number.isInteger(retries) || retries < 1 || retries > 5) throw new Error("Invalid attempt limit"); for (let i = 0; i < retries; i++) { options?.signal?.throwIfAborted(); try { return await fetchWithErrorHandling(url, options); } catch (error) { const method = (options?.method ?? "GET").toUpperCase(); const status = error instanceof ApiError ? error.status : -1; const transient = status === 0 || status === 408 || status >= 500; if (!["GET", "HEAD"].includes(method) || !transient || options?.signal?.aborted || i === retries - 1) throw error; // Exponential backoff await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000)); } } }; ``` --- ### 4. Authentication **Token management**: ```tsx 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(); const target = new URL(url); if (target.origin !== "https://api.example.com" || target.username || target.password) { throw new Error("Untrusted authenticated API destination"); } const headers = new Headers(options.headers); if (!token) throw new Error("Authentication required"); headers.set("Authorization", `Bearer ${token}`); return fetch(target, { ...options, headers, redirect: "error" }); }; ``` **Token refresh**: ```tsx let isRefreshing = false; let refreshPromise: Promise | null = null; const getValidToken = async (): Promise => { const token = await auth.getToken(); if (!token || isTokenExpired(token)) { if (!isRefreshing) { isRefreshing = true; refreshPromise = refreshToken().finally(() => { isRefreshing = false; refreshPromise = null; }); } return refreshPromise!; } return token; }; ``` --- ### 5. Offline Support **Check network status**: ```tsx 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**: ```tsx 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 ``` --- ### 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 // .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**: ```tsx // .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**: ```tsx // 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 (path: string): Promise => { const response = await fetch(`${BASE_URL}${path}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); }, post: async (path: string, body: unknown): Promise => { 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**: ```tsx // types/env.d.ts declare global { namespace NodeJS { interface ProcessEnv { EXPO_PUBLIC_API_URL: string; EXPO_PUBLIC_API_VERSION?: string; } } } export {}; ``` --- ### 7. Request Cancellation **Cancel on unmount**: ```tsx useEffect(() => { const controller = new AbortController(); fetch(url, { signal: controller.signal }) .then((response) => response.json()) .then((data) => { if (!controller.signal.aborted) setData(data); }) .catch((error) => { if (!controller.signal.aborted && error.name !== "AbortError") { setError(error); } }); return () => controller.abort(); }, [url]); ``` **With React Query** (automatic): ```tsx // Pass the query function signal to fetch; unmount/invalidation alone // does not guarantee cancellation of the underlying network operation. ``` --- ## Decision Tree ``` User asks about networking |-- Route-level data loading (web, SDK 55+)? | \-- Expo Router loaders — see references/expo-router-loaders.md | |-- 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 const data = await fetch(url).then((r) => r.json()); ``` **Right: Check response status** ```tsx const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); ``` **Wrong: Storing tokens in AsyncStorage** ```tsx await AsyncStorage.setItem("token", token); // Not secure! ``` **Right: Use SecureStore for sensitive data** ```tsx await SecureStore.setItemAsync("token", token); ```