N

native-data-fetching

Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, axios, React Query, SWR, error handling, caching strategies, offline support.

技能概览

名称

native-data-fetching

版本

v1.0.0

作者

expo

更新日期

2026-01-29T17:01:37.916Z

SKILL.md


Expo Networking


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 EXPOPUBLIC env vars with .env.development and .env.production files


    User: "Where should I put my API key?"

    -> Client-safe keys: EXPOPUBLIC in .env. Secret keys: non-prefixed env vars in API routes only