AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive framework for handling network operations in Expo projects. It emphasizes robust error handling, efficient state management using tools like React Query, and proper UI state design (loading, error, empty, content). It covers essential topics including authentication token management, offline support, environment variable configuration, and route-level data loading for web.
What it does
- Defines patterns for API requests using the fetch API.
- Provides guidance on integrating TanStack Query for caching and state management.
- Establishes requirements for handling loading, error, and empty UI states.
- Outlines secure practices for token management and environment variable usage.
- Explains how to implement offline support and request cancellation.
- Details route-level data loading for Expo Router web applications.
When to use it
- When implementing any network request or API integration.
- When setting up data fetching strategies for new screens.
- When debugging network failures or performance issues.
- When configuring authentication flows or environment-specific API URLs.
- When designing user interfaces that depend on remote data.
Advantages
- Promotes consistent error handling and UI state management across the app.
- Reduces common bugs related to unhandled network states or insecure storage.
- Provides clear decision trees for choosing between fetch, React Query, or loaders.
- Ensures secure handling of sensitive data and environment variables.
Usage examples
- “How do I make API calls in React Native?”
- The agent will recommend using the fetch API, wrapping calls in error handling logic, and potentially using React Query for state management.
- “My app needs to work offline, what should I do?”
- The agent will suggest using NetInfo for status detection and configuring React Query for offline persistence.
- “How do I handle authentication tokens securely?”
- The agent will advise using expo-secure-store for token storage and implementing a refresh flow for expired tokens.
- “How do I load data for a page in Expo Router?”
- The agent will guide you to use route-level loaders for web or React Query for native navigation.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/expo/expo-data-fetching/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip expo-data-fetching.zip -d .agents/skills
Documentation by Expo
Expo Networking
You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.
References
Consult these resources as needed:
references/
expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+)
offline-and-cancellation.md NetInfo network status, offline-first React Query, AbortControllerWhen to Use
Use this skill when:
Implementing API requests
Setting up data fetching (React Query, SWR)
Using Expo Router data loaders (
useLoaderData, web SDK 55+)Debugging network failures
Implementing caching strategies
Handling offline scenarios
Authentication/token management
Configuring API URLs and environment variables
Preferences
Avoid axios, prefer expo/fetch
Every Screen Has Four States
Design loading, error, empty, and content for screens that load data. These can overlap: a refresh error should coexist with cached content.
Loading ≠ empty. Empty means resolved with zero items, not missing data. Handle initial loading, failure, and hydration before checking list length. In TanStack Query v5,
isLoadingmeans the first fetch is running; a disabled or offline-paused query can have no data without being loading. Show the prerequisite or offline state in that case.Empty is a designed state, not a blank list. Use
ListEmptyComponenton FlatList/FlashList: explain why it is empty and offer the relevant next action. "No items yet" can offer Create; "No results" should offer changing or clearing the search/filter.Refetches keep stale content. Render cached
dataeven if a refresh fails, with a nonblocking error and retry. UseisLoadingfor first-fetch spinners andisFetchingfor background activity; prefer a skeleton for a slow initial load with a known layout, andRefreshControlfor user-initiated refresh.Gate on hydration. When initial UI or a redirect depends on persisted state (auth token, onboarding flag), the root layout renders nothing - or the splash - until that state has loaded. Deciding on unhydrated state flashes the wrong screen on every cold start and misroutes deep links that arrive before hydration.
Saves preserve work. While a mutation is pending, disable repeat submission. On failure, retain the draft, show an inline error, and let the user retry; clear or dismiss only after success. If updating optimistically, restore the previous value or mark the edit as unsynced on failure. Verify with a failed save followed by retry.
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, fetchStatus, error, refetch } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
if (data === undefined) {
if (error) return <ErrorState message={error.message} onRetry={() => refetch()} />;
if (fetchStatus === "paused") return <OfflineState />;
return <Loading />;
}
return (
<>
{error && <InlineError message="Could not refresh. Showing saved data." onRetry={() => refetch()} />}
{data === null ? <EmptyState message="User not found" /> : <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) => {
if (mutation.isPending) return;
mutation.mutate(data);
};
// Form keeps its draft on error and disables Submit while isLoading.
return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} error={mutation.error?.message} />;
}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
Network-status detection with NetInfo and offline-first React Query setup: see ./references/offline-and-cancellation.md.
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.comCreating 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 bundleNever put secrets (API keys with write access, database passwords) in
EXPO_PUBLIC_variables—they're visible in the built appEnvironment variables are inlined at build time, not runtime
Restart the dev server after changing
.envfilesFor 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
AbortController on unmount (React Query cancels automatically): see ./references/offline-and-cancellation.md.
Decision Tree
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 QueryCommon 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
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.
Submitting Feedback
If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-data-fetching" "<actionable feedback>"Only submit when you have something specific and actionable to report. Include as much relevant context as possible.
If an agent repeatedly fails at an Expo task or the user has to step in, submit a brief report of the task, expected result, and what went wrong using --category evals and the relevant Expo package or command as --subject.
Files
- ATTRIBUTION.mdAdded by AutoGineer823 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md13.3 KiB
- agents/openai.yaml421 B
- references/expo-router-loaders.md9.6 KiB
- references/offline-and-cancellation.md1.4 KiB
ATTRIBUTION.md
Attribution
This folder is an unmodified copy of the Agent Skill "expo-data-fetching", distributed by AutoGineer (https://autogineer.com/agent-skills).
Commit: a9e5bd3fa00b4e61f632e43f85da73aee1942582
Published by: Expo (https://github.com/expo)
Publisher: Expo (https://github.com/expo)
Licence: MIT License (https://spdx.org/licenses/MIT.html)
Licence evidence: declared in the frontmatter ("license: MIT")
Retrieved: 2026-11-07
The skill's own files are unmodified copies. AutoGineer is not affiliated with or endorsed by the author or the publisher.
Files added by AutoGineer
ATTRIBUTION.md: this file, written by AutoGineer
LICENSE: copied from plugins/expo/LICENSE
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs network access, Needs an API key, Needs Node.js
Security
What our scanner noticed· Low risk
- Instructions that use the network
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Expo
- Publisher
- Expo (Official)
- Source
- expo/skills · plugins/expo/skills/expo-data-fetching · commit a9e5bd3
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/expo/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Expo.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download expo-data-fetching.zip