Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Official Expo skill for networking and data fetching patterns in Expo/React Native apps.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: native-data-fetching3description: 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`).4version: 1.0.05license: MIT6---78# Expo Networking910**You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.**1112## References1314Consult these resources as needed:1516```17references/18expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+)19```2021## When to Use2223Use this skill when:2425- Implementing API requests26- Setting up data fetching (React Query, SWR)27- Using Expo Router data loaders (`useLoaderData`, web SDK 55+)28- Debugging network failures29- Implementing caching strategies30- Handling offline scenarios31- Authentication/token management32- Configuring API URLs and environment variables3334## Preferences3536- Avoid axios, prefer expo/fetch3738## Common Issues & Solutions3940### 1. Basic Fetch Usage4142**Simple GET request**:4344```tsx45const fetchUser = async (userId: string) => {46const response = await fetch(`https://api.example.com/users/${userId}`);4748if (!response.ok) {49throw new Error(`HTTP error! status: ${response.status}`);50}5152return response.json();53};54```5556**POST request with body**:5758```tsx59const createUser = async (userData: UserData) => {60const response = await fetch("https://api.example.com/users", {61method: "POST",62headers: {63"Content-Type": "application/json",64Authorization: `Bearer ${token}`,65},66body: JSON.stringify(userData),67});6869if (!response.ok) {70const error = await response.json();71throw new Error(error.message);72}7374return response.json();75};76```7778---7980### 2. React Query (TanStack Query)8182**Setup**:8384```tsx85// app/_layout.tsx86import { QueryClient, QueryClientProvider } from "@tanstack/react-query";8788const queryClient = new QueryClient({89defaultOptions: {90queries: {91staleTime: 1000 * 60 * 5, // 5 minutes92retry: 2,93},94},95});9697export default function RootLayout() {98return (99<QueryClientProvider client={queryClient}>100<Stack />101</QueryClientProvider>102);103}104```105106**Fetching data**:107108```tsx109import { useQuery } from "@tanstack/react-query";110111function UserProfile({ userId }: { userId: string }) {112const { data, isLoading, error, refetch } = useQuery({113queryKey: ["user", userId],114queryFn: () => fetchUser(userId),115});116117if (isLoading) return <Loading />;118if (error) return <Error message={error.message} />;119120return <Profile user={data} />;121}122```123124**Mutations**:125126```tsx127import { useMutation, useQueryClient } from "@tanstack/react-query";128129function CreateUserForm() {130const queryClient = useQueryClient();131132const mutation = useMutation({133mutationFn: createUser,134onSuccess: () => {135// Invalidate and refetch136queryClient.invalidateQueries({ queryKey: ["users"] });137},138});139140const handleSubmit = (data: UserData) => {141mutation.mutate(data);142};143144return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} />;145}146```147148---149150### 3. Error Handling151152**Comprehensive error handling**:153154```tsx155class ApiError extends Error {156constructor(message: string, public status: number, public code?: string) {157super(message);158this.name = "ApiError";159}160}161162const fetchWithErrorHandling = async (url: string, options?: RequestInit) => {163try {164const response = await fetch(url, options);165166if (!response.ok) {167const error = await response.json().catch(() => ({}));168throw new ApiError(169error.message || "Request failed",170response.status,171error.code172);173}174175return response.json();176} catch (error) {177if (error instanceof ApiError) {178throw error;179}180// Network error (no internet, timeout, etc.)181throw new ApiError("Network error", 0, "NETWORK_ERROR");182}183};184```185186**Retry logic**:187188```tsx189const fetchWithRetry = async (190url: string,191options?: RequestInit,192retries = 3193) => {194for (let i = 0; i < retries; i++) {195try {196return await fetchWithErrorHandling(url, options);197} catch (error) {198if (i === retries - 1) throw error;199// Exponential backoff200await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));201}202}203};204```205206---207208### 4. Authentication209210**Token management**:211212```tsx213import * as SecureStore from "expo-secure-store";214215const TOKEN_KEY = "auth_token";216217export const auth = {218getToken: () => SecureStore.getItemAsync(TOKEN_KEY),219setToken: (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token),220removeToken: () => SecureStore.deleteItemAsync(TOKEN_KEY),221};222223// Authenticated fetch wrapper224const authFetch = async (url: string, options: RequestInit = {}) => {225const token = await auth.getToken();226227return fetch(url, {228...options,229headers: {230...options.headers,231Authorization: token ? `Bearer ${token}` : "",232},233});234};235```236237**Token refresh**:238239```tsx240let isRefreshing = false;241let refreshPromise: Promise<string> | null = null;242243const getValidToken = async (): Promise<string> => {244const token = await auth.getToken();245246if (!token || isTokenExpired(token)) {247if (!isRefreshing) {248isRefreshing = true;249refreshPromise = refreshToken().finally(() => {250isRefreshing = false;251refreshPromise = null;252});253}254return refreshPromise!;255}256257return token;258};259```260261---262263### 5. Offline Support264265**Check network status**:266267```tsx268import NetInfo from "@react-native-community/netinfo";269270// Hook for network status271function useNetworkStatus() {272const [isOnline, setIsOnline] = useState(true);273274useEffect(() => {275return NetInfo.addEventListener((state) => {276setIsOnline(state.isConnected ?? true);277});278}, []);279280return isOnline;281}282```283284**Offline-first with React Query**:285286```tsx287import { onlineManager } from "@tanstack/react-query";288import NetInfo from "@react-native-community/netinfo";289290// Sync React Query with network status291onlineManager.setEventListener((setOnline) => {292return NetInfo.addEventListener((state) => {293setOnline(state.isConnected ?? true);294});295});296297// Queries will pause when offline and resume when online298```299300---301302### 6. Environment Variables303304**Using environment variables for API configuration**:305306Expo supports environment variables with the `EXPO_PUBLIC_` prefix. These are inlined at build time and available in your JavaScript code.307308```tsx309// .env310EXPO_PUBLIC_API_URL=https://api.example.com311EXPO_PUBLIC_API_VERSION=v1312313// Usage in code314const API_URL = process.env.EXPO_PUBLIC_API_URL;315316const fetchUsers = async () => {317const response = await fetch(`${API_URL}/users`);318return response.json();319};320```321322**Environment-specific configuration**:323324```tsx325// .env.development326EXPO_PUBLIC_API_URL=http://localhost:3000327328// .env.production329EXPO_PUBLIC_API_URL=https://api.production.com330```331332**Creating an API client with environment config**:333334```tsx335// api/client.ts336const BASE_URL = process.env.EXPO_PUBLIC_API_URL;337338if (!BASE_URL) {339throw new Error("EXPO_PUBLIC_API_URL is not defined");340}341342export const apiClient = {343get: async <T,>(path: string): Promise<T> => {344const response = await fetch(`${BASE_URL}${path}`);345if (!response.ok) throw new Error(`HTTP ${response.status}`);346return response.json();347},348349post: async <T,>(path: string, body: unknown): Promise<T> => {350const response = await fetch(`${BASE_URL}${path}`, {351method: "POST",352headers: { "Content-Type": "application/json" },353body: JSON.stringify(body),354});355if (!response.ok) throw new Error(`HTTP ${response.status}`);356return response.json();357},358};359```360361**Important notes**:362363- Only variables prefixed with `EXPO_PUBLIC_` are exposed to the client bundle364- Never put secrets (API keys with write access, database passwords) in `EXPO_PUBLIC_` variables—they're visible in the built app365- Environment variables are inlined at **build time**, not runtime366- Restart the dev server after changing `.env` files367- For server-side secrets in API routes, use variables without the `EXPO_PUBLIC_` prefix368369**TypeScript support**:370371```tsx372// types/env.d.ts373declare global {374namespace NodeJS {375interface ProcessEnv {376EXPO_PUBLIC_API_URL: string;377EXPO_PUBLIC_API_VERSION?: string;378}379}380}381382export {};383```384385---386387### 7. Request Cancellation388389**Cancel on unmount**:390391```tsx392useEffect(() => {393const controller = new AbortController();394395fetch(url, { signal: controller.signal })396.then((response) => response.json())397.then(setData)398.catch((error) => {399if (error.name !== "AbortError") {400setError(error);401}402});403404return () => controller.abort();405}, [url]);406```407408**With React Query** (automatic):409410```tsx411// React Query automatically cancels requests when queries are invalidated412// or components unmount413```414415---416417## Decision Tree418419```420User asks about networking421|-- Route-level data loading (web, SDK 55+)?422| \-- Expo Router loaders — see references/expo-router-loaders.md423|424|-- Basic fetch?425| \-- Use fetch API with error handling426|427|-- Need caching/state management?428| |-- Complex app -> React Query (TanStack Query)429| \-- Simpler needs -> SWR or custom hooks430|431|-- Authentication?432| |-- Token storage -> expo-secure-store433| \-- Token refresh -> Implement refresh flow434|435|-- Error handling?436| |-- Network errors -> Check connectivity first437| |-- HTTP errors -> Parse response, throw typed errors438| \-- Retries -> Exponential backoff439|440|-- Offline support?441| |-- Check status -> NetInfo442| \-- Queue requests -> React Query persistence443|444|-- Environment/API config?445| |-- Client-side URLs -> EXPO_PUBLIC_ prefix in .env446| |-- Server secrets -> Non-prefixed env vars (API routes only)447| \-- Multiple environments -> .env.development, .env.production448|449\-- Performance?450|-- Caching -> React Query with staleTime451|-- Deduplication -> React Query handles this452\-- Cancellation -> AbortController or React Query453```454455## Common Mistakes456457**Wrong: No error handling**458459```tsx460const data = await fetch(url).then((r) => r.json());461```462463**Right: Check response status**464465```tsx466const response = await fetch(url);467if (!response.ok) throw new Error(`HTTP ${response.status}`);468const data = await response.json();469```470471**Wrong: Storing tokens in AsyncStorage**472473```tsx474await AsyncStorage.setItem("token", token); // Not secure!475```476477**Right: Use SecureStore for sensitive data**478479```tsx480await SecureStore.setItemAsync("token", token);481```482483## Example Invocations484485User: "How do I make API calls in React Native?"486-> Use fetch, wrap with error handling487488User: "Should I use React Query or SWR?"489-> React Query for complex apps, SWR for simpler needs490491User: "My app needs to work offline"492-> Use NetInfo for status, React Query persistence for caching493494User: "How do I handle authentication tokens?"495-> Store in expo-secure-store, implement refresh flow496497User: "API calls are slow"498-> Check caching strategy, use React Query staleTime499500User: "How do I configure different API URLs for dev and prod?"501-> Use EXPO*PUBLIC* env vars with .env.development and .env.production files502503User: "Where should I put my API key?"504-> Client-safe keys: EXPO*PUBLIC* in .env. Secret keys: non-prefixed env vars in API routes only505506User: "How do I load data for a page in Expo Router?"507-> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch.508