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).
.claude/skills/lingxling-native-data-fetching/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 119% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-20 | ✓→✓ | = Same ✓ | 122% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 121% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 297% | 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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-20 | pass→pass | 18,162 | 19,641 | +8% | 1 | 1 | 0% | 2,849 | 6,313 | +122% | 0 | 0 | — |
case-01 | fail→pass | 14,113 | 21,073 | +49% | 1 | 1 | 0% | 2,327 | 5,102 | +119% | 0 | 0 | — |
case-02 | pass→pass | 15,284 | 9,902 | -35% | 1 | 1 | 0% | 2,078 | 4,599 | +121% | 0 | 0 | — |
case-03 | pass→pass | 6,863 | 5,028 | -27% | 1 | 1 | 0% | 960 | 3,809 | +297% | 0 | 0 | — |
case-04 | pass→pass | 14,824 | 9,543 | -36% | 1 | 1 | 0% | 2,039 | 5,117 | +151% | 0 | 0 | — |
case-05 | pass→pass | 31,121 | 12,693 | -59% | 1 | 1 | 0% | 1,870 | 5,023 | +169% | 0 | 0 | — |
case-06 | fail→pass | 14,903 | 9,535 | -36% | 1 | 1 | 0% | 2,200 | 4,971 | +126% | 0 | 0 | — |
case-07 | pass→pass | 14,414 | 27,960 | +94% | 1 | 1 | 0% | 1,995 | 4,818 | +142% | 0 | 0 | — |
case-08 | pass→pass | 14,785 | 22,471 | +52% | 1 | 1 | 0% | 2,238 | 6,067 | +171% | 0 | 0 | — |
case-09 | pass→pass | 15,554 | 12,387 | -20% | 1 | 1 | 0% | 2,266 | 5,849 | +158% | 0 | 0 | — |
case-10 | pass→pass | 11,248 | 9,535 | -15% | 1 | 1 | 0% | 2,115 | 5,128 | +142% | 0 | 0 | — |
case-11 | pass→pass | 8,049 | 10,788 | +34% | 1 | 1 | 0% | 1,301 | 3,950 | +204% | 0 | 0 | — |
case-12 | pass→pass | 10,940 | 5,286 | -52% | 1 | 1 | 0% | 1,470 | 4,213 | +187% | 0 | 0 | — |
case-13 | pass→pass | 16,854 | 15,421 | -9% | 1 | 1 | 0% | 3,072 | 5,827 | +90% | 0 | 0 | — |
case-14 | pass→pass | 14,163 | 8,023 | -43% | 1 | 1 | 0% | 1,989 | 4,726 | +138% | 0 | 0 | — |
case-15 | pass→pass | 10,697 | 8,948 | -16% | 1 | 1 | 0% | 1,867 | 4,806 | +157% | 0 | 0 | — |
case-16 | pass→pass | 8,911 | 10,054 | +13% | 1 | 1 | 0% | 1,514 | 4,592 | +203% | 0 | 0 | — |
case-17 | pass→pass | 20,653 | 21,070 | +2% | 1 | 1 | 0% | 3,136 | 6,530 | +108% | 0 | 0 | — |
case-18 | pass→pass | 11,300 | 23,332 | +106% | 1 | 1 | 0% | 1,518 | 4,122 | +172% | 0 | 0 | — |
case-19 | pass→pass | 12,341 | 13,455 | +9% | 1 | 1 | 0% | 1,957 | 5,183 | +165% | 0 | 0 | — |
case-21 | pass→pass | 10,051 | 10,137 | +1% | 1 | 1 | 0% | 1,913 | 5,247 | +174% | 0 | 0 | — |
case-22 | pass→pass | 8,892 | 8,605 | -3% | 1 | 1 | 0% | 1,670 | 4,922 | +195% | 0 | 0 | — |
case-23 | pass→pass | 11,533 | 13,337 | +16% | 1 | 1 | 0% | 2,173 | 5,303 | +144% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.