expo-data-fetching

作者: expo

Framework (OSS). 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`).

npx skills add https://github.com/expo/skills --skill expo-data-fetching

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, AbortController

When 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

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

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.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

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 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

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 submit-expo-feedback@latest --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.

來自 expo 的更多技能

expo-upgrade
expo
框架(開源)。升級 Expo SDK 版本與修復相依性問題的指南。
apidevelopmentofficial
android-e2e-testing
expo
使用 ADB 在 Android 模擬器上測試 Expo Router 功能。在實作原生 Android 功能後或驗證 Android 上的 UI 行為時使用。
official
expo-dev-client
expo
透過 EAS Build 或在本機端建立自訂 Expo 開發用戶端,以便在實體裝置上測試原生程式碼。僅在使用自訂原生模組、Apple 目標(小工具、App Clips)或 Expo Go 中未包含的第三方原生程式碼時才需要;請先嘗試使用 npx expo start 執行 Expo Go。支援雲端建置並自動提交至 TestFlight,或在本機端建置,輸出 .ipa(iOS)或 .apk / .aab(Android)檔案。需要在 eas.json 中設定包含開發設定檔的組態,該設定檔會設定...
official
expo-tailwind-setup
expo
適用於 iOS、Android 及 Web 的 Expo 應用程式通用 Tailwind CSS v4 樣式。透過 Metro 轉換器整合 Tailwind CSS v4 與 react-native-css 及 NativeWind v5,無需 Babel 配置。提供可重複使用的 CSS 封裝元件庫(View、Text、ScrollView、Pressable、TextInput、Image、Link),可將 className 樣式套用至原生元素。支援透過 CSS 媒體查詢進行平台特定樣式設定、透過 @theme 區塊自訂主題變數,以及 Apple 語意...
official
android-jetpack-compose
expo
在構建 Android UI 時使用 Jetpack Compose,透過 remember/mutableStateOf 管理狀態,或實作宣告式 UI 模式。
official
swiftui-expert-skill
expo
撰寫、審查或改進 SwiftUI 程式碼,遵循狀態管理、視圖組合、效能、macOS 特定 API 以及 iOS 26+ Liquid… 的最佳實踐。
official
eas-update-insights
expo
檢查已發布的 EAS 更新的健康狀況:崩潰率、安裝/啟動次數、唯一用戶數、有效載荷大小,以及每個…的嵌入式與 OTA 用戶分佈。
official
expo-module
expo
使用 Expo Modules API(Swift、Kotlin、TypeScript)建立與撰寫 Expo 原生模組與檢視的指南。涵蓋模組定義 DSL、原生…
official