native-data-fetching
Install this skill
npx skills add expo/skillsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
Native data fetching in Expo provides a direct, low-overhead approach to communicating with remote servers using the global Fetch API. This method avoids third-party HTTP libraries by prioritizing standard web APIs, which ensures better compatibility with the Expo managed environment and smaller bundle sizes. It focuses on clean implementation of asynchronous requests, structured response parsing, and standard HTTP error handling. When integrated with state management libraries like TanStack Query, it creates a predictable data layer that supports caching, revalidation, and loading state management. Developers maintain control over headers, authentication flows, and retry logic without unnecessary abstractions, keeping mobile networking predictable and efficient. This core skill serves as the foundation for any application requirement involving API synchronization, persistent session management, or remote resource interaction within an Expo ecosystem.
When to Use This Skill
- •Retrieving JSON data from RESTful endpoints for list views
- •Submitting form data via authenticated POST requests
- •Refreshing expired access tokens during background API calls
- •Caching frequently accessed configuration data locally
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “how to fetch data in expo
- “setup react query for api requests in expo
- “implement authorized api call with secure store
- “best way to handle api errors in expo fetch
- “create a reusable fetch helper for mobile app
Pro Tips
- 💡Prioritize `expo/fetch` over `axios` for Expo-specific projects to leverage platform optimizations and reduce bundle size.
- 💡Always implement comprehensive error handling, including network failures, HTTP status codes, and server-side errors, to provide a resilient user experience.
- 💡Strategically implement caching (e.g., with React Query or SWR) to reduce redundant requests and significantly improve application responsiveness and offline capabilities.
What this skill does
- •Perform standard HTTP GET, POST, PUT, and DELETE operations
- •Manage secure token persistence with expo-secure-store
- •Implement request retry strategies with exponential backoff
- •Integrate with React Query for automated caching and state hydration
- •Handle non-200 HTTP response codes through explicit error throwing
When not to use it
- ✕When using WebSockets or long-lived streaming connections
- ✕For complex binary data uploads that require low-level progress tracking
Example workflow
- Define a centralized fetch utility that adds authorization headers
- Wrap the application root with a QueryClientProvider
- Create a hook or function using the native fetch wrapper to get resource data
- Use useQuery in components to manage loading, error, and success states
- Invalidate relevant queries after successful mutations
Prerequisites
- –Basic knowledge of TypeScript
- –Understanding of React asynchronous state patterns
Pitfalls & limitations
- !Failing to check response.ok, as fetch does not reject on HTTP errors
- !Memory leaks when triggering fetches on every component render without a query provider
- !Insecure storage of sensitive tokens using standard AsyncStore
FAQ
How it compares
Unlike generic web fetching, this skill emphasizes Expo-specific security patterns like SecureStore and standardizes integration with TanStack Query to ensure mobile-optimized caching.
📄 Full skill instructions — original source: expo/skills
**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**:
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();
};---
### 2. React Query (TanStack Query)
**Setup**:
// 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} />;
}---
### 3. Error Handling
**Comprehensive error handling**:
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));
}
}
};---
### 4. Authentication
**Token management**:
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;
};---
### 5. Offline Support
**Check network status**:
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---
### 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.// .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();
},
};**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**:
// 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**:
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---
## 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**
const data = await fetch(url).then((r) => r.json());**Right: Check response status**
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);## 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
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/native-data-fetching/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/expo/skills/native-data-fetching/SKILL.md - Cursor:
~/.cursor/skills/expo/skills/native-data-fetching/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/expo/skills/native-data-fetching/SKILL.md
🚀 Install with CLI:npx skills add expo/skills