expo-data-fetching

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Expo Networking

Expo 网络处理

You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.
任何涉及网络的工作(包括API请求、数据获取、缓存或网络调试)都必须使用此技能。

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
按需查阅以下资源:
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
在以下场景中使用此技能:
  • 实现API请求
  • 配置数据获取(React Query、SWR)
  • 使用Expo Router数据加载器(
    useLoaderData
    ,Web SDK 55+)
  • 调试网络故障
  • 实现缓存策略
  • 处理离线场景
  • 身份验证/令牌管理
  • 配置API URL和环境变量

Preferences

偏好建议

  • Avoid axios, prefer expo/fetch
  • 避免使用axios,优先选择expo/fetch

Common Issues & Solutions

常见问题与解决方案

1. Basic Fetch Usage

1. 基础Fetch用法

Simple GET request:
tsx
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:
tsx
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();
};

简单GET请求:
tsx
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请求:
tsx
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)

2. React Query(TanStack Query)

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:
tsx
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:
tsx
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} />;
}

配置:
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>
  );
}
数据获取:
tsx
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} />;
}
数据变更:
tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";

function CreateUserForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      // 失效并重新获取数据
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  const handleSubmit = (data: UserData) => {
    mutation.mutate(data);
  };

  return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} />;
}

3. Error Handling

3. 错误处理

Comprehensive error handling:
tsx
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:
tsx
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));
    }
  }
};

全面错误处理:
tsx
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;
    }
    // 网络错误(无网络、超时等)
    throw new ApiError("Network error", 0, "NETWORK_ERROR");
  }
};
重试逻辑:
tsx
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;
      // 指数退避
      await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
    }
  }
};

4. Authentication

4. 身份验证

Token management:
tsx
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:
tsx
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;
};

令牌管理:
tsx
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),
};

// 带身份验证的fetch封装
const authFetch = async (url: string, options: RequestInit = {}) => {
  const token = await auth.getToken();

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: token ? `Bearer ${token}` : "",
    },
  });
};
令牌刷新:
tsx
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

5. 离线支持

Network-status detection with NetInfo and offline-first React Query setup: see ./references/offline-and-cancellation.md.

使用NetInfo检测网络状态并配置离线优先的React Query:请查看./references/offline-and-cancellation.md

6. Environment Variables

6. 环境变量

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:
  • 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:
tsx
// types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      EXPO_PUBLIC_API_URL: string;
      EXPO_PUBLIC_API_VERSION?: string;
    }
  }
}

export {};

使用环境变量配置API:
Expo支持带有
EXPO_PUBLIC_
前缀的环境变量,这些变量会在构建时内联到代码中,可在JavaScript代码中访问。
tsx
// .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_API_VERSION=v1

// 代码中的使用方式
const API_URL = process.env.EXPO_PUBLIC_API_URL;

const fetchUsers = async () => {
  const response = await fetch(`${API_URL}/users`);
  return response.json();
};
环境特定配置:
tsx
// .env.development
EXPO_PUBLIC_API_URL=http://localhost:3000

// .env.production
EXPO_PUBLIC_API_URL=https://api.production.com
创建带有环境配置的API客户端:
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();
  },
};
重要说明:
  • 只有带有
    EXPO_PUBLIC_
    前缀的变量会暴露给客户端包
  • 切勿将敏感信息(具有写入权限的API密钥、数据库密码)放入
    EXPO_PUBLIC_
    变量中——它们会在构建后的应用中可见
  • 环境变量是在构建时内联的,而非运行时
  • 修改
    .env
    文件后需重启开发服务器
  • 对于API路由中的服务器端敏感信息,请使用不带
    EXPO_PUBLIC_
    前缀的变量
TypeScript支持:
tsx
// types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      EXPO_PUBLIC_API_URL: string;
      EXPO_PUBLIC_API_VERSION?: string;
    }
  }
}

export {};

7. Request Cancellation

7. 请求取消

AbortController on unmount (React Query cancels automatically): see ./references/offline-and-cancellation.md.

组件卸载时使用AbortController(React Query会自动取消请求):请查看./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
用户询问网络相关问题
  |-- 是否为路由级数据加载(Web,SDK 55+)?
  |   \-- 使用Expo Router加载器 — 查看references/expo-router-loaders.md
  |
  |-- 是否为基础fetch?
  |   \-- 使用带有错误处理的fetch API
  |
  |-- 是否需要缓存/状态管理?
  |   |-- 复杂应用 -> React Query(TanStack Query)
  |   \-- 简单需求 -> SWR或自定义钩子
  |
  |-- 是否涉及身份验证?
  |   |-- 令牌存储 -> 使用expo-secure-store
  |   \-- 令牌刷新 -> 实现刷新流程
  |
  |-- 是否需要错误处理?
  |   |-- 网络错误 -> 先检查连接状态
  |   |-- HTTP错误 -> 解析响应,抛出类型化错误
  |   \-- 重试 -> 使用指数退避策略
  |
  |-- 是否需要离线支持?
  |   |-- 状态检测 -> 使用NetInfo
  |   \-- 请求队列 -> 使用React Query持久化
  |
  |-- 是否需要环境/API配置?
  |   |-- 客户端URL -> 在.env中使用EXPO_PUBLIC_前缀
  |   |-- 服务器敏感信息 -> 使用无前缀的环境变量(仅API路由)
  |   \-- 多环境配置 -> 使用.env.development和.env.production
  |
  \-- 是否关注性能?
      |-- 缓存 -> 为React Query设置staleTime
      |-- 请求去重 -> React Query自动处理
      \-- 请求取消 -> 使用AbortController或React Query

Common Mistakes

常见错误

Wrong: No error handling
tsx
const data = await fetch(url).then((r) => r.json());
Right: Check response status
tsx
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
Wrong: Storing tokens in AsyncStorage
tsx
await AsyncStorage.setItem("token", token); // Not secure!
Right: Use SecureStore for sensitive data
tsx
await SecureStore.setItemAsync("token", token);
错误示例:无错误处理
tsx
const data = await fetch(url).then((r) => r.json());
正确示例:检查响应状态
tsx
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
错误示例:在AsyncStorage中存储令牌
tsx
await AsyncStorage.setItem("token", token); // 不安全!
正确示例:使用SecureStore存储敏感数据
tsx
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.
用户:"我如何在React Native中发起API调用?" -> 使用fetch,并添加错误处理
用户:"我应该使用React Query还是SWR?" -> 复杂应用使用React Query,简单需求使用SWR
用户:"我的应用需要支持离线使用" -> 使用NetInfo检测状态,使用React Query持久化实现缓存
用户:"我如何处理身份验证令牌?" -> 存储在expo-secure-store中,实现令牌刷新流程
用户:"API调用速度很慢" -> 检查缓存策略,为React Query设置staleTime 用户:"我如何为开发环境和生产环境配置不同的API URL?" -> 使用带有
EXPO_PUBLIC_
前缀的环境变量,搭配.env.development和.env.production文件 用户:"我的API密钥应该放在哪里?" -> 客户端安全密钥:放在.env的
EXPO_PUBLIC_
变量中。敏感密钥:仅在API路由中使用无前缀的环境变量
用户:"我如何在Expo Router中为页面加载数据?" -> 查看references/expo-router-loaders.md了解路由级加载器(Web,SDK 55+)。原生平台使用React Query或fetch。