--- name: native-data-fetching description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, axios, React Query, SWR, error handling, caching strategies, offline support. version: 1.0.0 license: MIT --- # 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 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**: ```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, isLoading, error, refetch } = useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId), }); if (isLoading) return ; if (error) return ; return ; } ``` **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) => { 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; } // Network error (no internet, timeout, etc.) throw new ApiError("Network error", 0, "NETWORK_ERROR"); } }; ``` **Retry logic**: ```tsx 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)); } } }; ``` --- ### 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(); return fetch(url, { ...options, headers: { ...options.headers, Authorization: token ? `Bearer ${token}` : "", }, }); }; ``` **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(setData) .catch((error) => { if (error.name !== "AbortError") { setError(error); } }); return () => controller.abort(); }, [url]); ``` **With React Query** (automatic): ```tsx // 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** ```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); ``` ## 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 EXPO*PUBLIC* env vars with .env.development and .env.production files User: "Where should I put my API key?" -> Client-safe keys: EXPO*PUBLIC* in .env. Secret keys: non-prefixed env vars in API routes only