Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 107% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 139% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 61% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 169% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 700% | 0% |
You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.
Consult these resources as needed:
references/
expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+)Use this skill when:
useLoaderData, web SDK 55+)Simple GET request:
tsxconst 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:
tsxconst 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(); };
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 ( <QueryClientProvider client={queryClient}> <Stack /> </QueryClientProvider> ); }
Fetching data:
tsximport { 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:
tsximport { 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} />; }
Comprehensive error handling:
tsxclass 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:
tsxconst 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)); } } };
Token management:
tsximport * 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:
tsxlet 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; };
Check network status:
tsximport 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:
tsximport { 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
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 <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:
EXPO_PUBLIC_ are exposed to the client bundleEXPO_PUBLIC_ variables—they're visible in the built app.env filesEXPO_PUBLIC_ prefixTypeScript support:
tsx// types/env.d.ts declare global { namespace NodeJS { interface ProcessEnv { EXPO_PUBLIC_API_URL: string; EXPO_PUBLIC_API_VERSION?: string; } } } export {};
Cancel on unmount:
tsxuseEffect(() => { 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
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 QueryWrong: No error handling
tsxconst data = await fetch(url).then((r) => r.json());
Right: Check response status
tsxconst response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json();
Wrong: Storing tokens in AsyncStorage
tsxawait AsyncStorage.setItem("token", token); // Not secure!
Right: Use SecureStore for sensitive data
tsxawait SecureStore.setItemAsync("token", token);
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
User: "How do I load data for a page in Expo Router?" -> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch.
Other measured skills in the registry, with their headline benchmark lift.