**You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.** Use this router when: - Implementing API requests
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(); };
// 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} />; }
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)); } } };
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; };
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
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(); }, };
EXPO_PUBLIC_ are exposed to the client bundleEXPO_PUBLIC_ variables—they're visible in the built app.env filesEXPO_PUBLIC_ prefix// types/env.d.ts declare global { namespace NodeJS { interface ProcessEnv { EXPO_PUBLIC_API_URL: string; EXPO_PUBLIC_API_VERSION?: string; } } } export {};
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
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
const data = await fetch(url).then((r) => r.json());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);