Expo Networking skill

Use when implementing or debugging ANY network request, API call, or data fetching.

by expo·MIT license·★ 2,657 Stars on the repo·GitHub ↗

Use now

Files of Expo Networking

expo/main1 file shown
SKILL.md
Show the full text479 lines

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

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, isLoading means 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 ListEmptyComponent on 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 data even if a refresh fails, with a nonblocking error and retry. Use isLoading for first-fetch spinners and isFetching for background activity; prefer a skeleton for a slow initial load with a known layout, and RefreshControl for 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.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. If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.

1---
2name: expo-data-fetching
3description: Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, loading/empty/error screen states, and Expo Router data loaders (`useLoaderData`).
4version: 1.0.0
5license: MIT
6---
7 
8# Expo Networking
9 
10**You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.**
11 
12## References
13 
14Consult these resources as needed:
15 
16```
17references/
18 expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+)
19 offline-and-cancellation.md NetInfo network status, offline-first React Query, AbortController
20```
21 
22## When to Use
23 
24Use this skill when:
25 
26- Implementing API requests
27- Setting up data fetching (React Query, SWR)
28- Using Expo Router data loaders (`useLoaderData`, web SDK 55+)
29- Debugging network failures
30- Implementing caching strategies
31- Handling offline scenarios
32- Authentication/token management
33- Configuring API URLs and environment variables
34 
35## Preferences
36 
37- Avoid axios, prefer expo/fetch
38 
39## Every Screen Has Four States
40 
41Design **loading**, **error**, **empty**, and **content** for screens that load data. These can overlap: a refresh error should coexist with cached content.
42 
43- **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, `isLoading` means 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.
44- **Empty is a designed state, not a blank list.** Use `ListEmptyComponent` on 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.
45- **Refetches keep stale content.** Render cached `data` even if a refresh fails, with a nonblocking error and retry. Use `isLoading` for first-fetch spinners and `isFetching` for background activity; prefer a skeleton for a slow initial load with a known layout, and `RefreshControl` for user-initiated refresh.
46- **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.
47 
48**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.
49 
50## Common Issues & Solutions
51 
52### 1. Basic Fetch Usage
53 
54**Simple GET request**:
55 
56```tsx
57const fetchUser = async (userId: string) => {
58 const response = await fetch(`https://api.example.com/users/${userId}`);
59 
60 if (!response.ok) {
61 throw new Error(`HTTP error! status: ${response.status}`);
62 }
63 
64 return response.json();
65};
66```
67 
68**POST request with body**:
69 
70```tsx
71const createUser = async (userData: UserData) => {
72 const response = await fetch("https://api.example.com/users", {
73 method: "POST",
74 headers: {
75 "Content-Type": "application/json",
76 Authorization: `Bearer ${token}`,
77 },
78 body: JSON.stringify(userData),
79 });
80 
81 if (!response.ok) {
82 const error = await response.json();
83 throw new Error(error.message);
84 }
85 
86 return response.json();
87};
88```
89 
90---
91 
92### 2. React Query (TanStack Query)
93 
94**Setup**:
95 
96```tsx
97// app/_layout.tsx
98import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
99 
100const queryClient = new QueryClient({
101 defaultOptions: {
102 queries: {
103 staleTime: 1000 * 60 * 5, // 5 minutes
104 retry: 2,
105 },
106 },
107});
108 
109export default function RootLayout() {
110 return (
111 <QueryClientProvider client={queryClient}>
112 <Stack />
113 </QueryClientProvider>
114 );
115}
116```
117 
118**Fetching data**:
119 
120```tsx
121import { useQuery } from "@tanstack/react-query";
122 
123function UserProfile({ userId }: { userId: string }) {
124 const { data, fetchStatus, error, refetch } = useQuery({
125 queryKey: ["user", userId],
126 queryFn: () => fetchUser(userId),
127 });
128 
129 if (data === undefined) {
130 if (error) return <ErrorState message={error.message} onRetry={() => refetch()} />;
131 if (fetchStatus === "paused") return <OfflineState />;
132 return <Loading />;
133 }
134 
135 return (
136 <>
137 {error && <InlineError message="Could not refresh. Showing saved data." onRetry={() => refetch()} />}
138 {data === null ? <EmptyState message="User not found" /> : <Profile user={data} />}
139 </>
140 );
141}
142```
143 
144**Mutations**:
145 
146```tsx
147import { useMutation, useQueryClient } from "@tanstack/react-query";
148 
149function CreateUserForm() {
150 const queryClient = useQueryClient();
151 
152 const mutation = useMutation({
153 mutationFn: createUser,
154 onSuccess: () => {
155 // Invalidate and refetch
156 queryClient.invalidateQueries({ queryKey: ["users"] });
157 },
158 });
159 
160 const handleSubmit = (data: UserData) => {
161 if (mutation.isPending) return;
162 mutation.mutate(data);
163 };
164 
165 // Form keeps its draft on error and disables Submit while isLoading.
166 return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} error={mutation.error?.message} />;
167}
168```
169 
170---
171 
172### 3. Error Handling
173 
174**Comprehensive error handling**:
175 
176```tsx
177class ApiError extends Error {
178 constructor(message: string, public status: number, public code?: string) {
179 super(message);
180 this.name = "ApiError";
181 }
182}
183 
184const fetchWithErrorHandling = async (url: string, options?: RequestInit) => {
185 try {
186 const response = await fetch(url, options);
187 
188 if (!response.ok) {
189 const error = await response.json().catch(() => ({}));
190 throw new ApiError(
191 error.message || "Request failed",
192 response.status,
193 error.code
194 );
195 }
196 
197 return response.json();
198 } catch (error) {
199 if (error instanceof ApiError) {
200 throw error;
201 }
202 // Network error (no internet, timeout, etc.)
203 throw new ApiError("Network error", 0, "NETWORK_ERROR");
204 }
205};
206```
207 
208**Retry logic**:
209 
210```tsx
211const fetchWithRetry = async (
212 url: string,
213 options?: RequestInit,
214 retries = 3
215) => {
216 for (let i = 0; i < retries; i++) {
217 try {
218 return await fetchWithErrorHandling(url, options);
219 } catch (error) {
220 if (i === retries - 1) throw error;
221 // Exponential backoff
222 await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
223 }
224 }
225};
226```
227 
228---
229 
230### 4. Authentication
231 
232**Token management**:
233 
234```tsx
235import * as SecureStore from "expo-secure-store";
236 
237const TOKEN_KEY = "auth_token";
238 
239export const auth = {
240 getToken: () => SecureStore.getItemAsync(TOKEN_KEY),
241 setToken: (token: string) => SecureStore.setItemAsync(TOKEN_KEY, token),
242 removeToken: () => SecureStore.deleteItemAsync(TOKEN_KEY),
243};
244 
245// Authenticated fetch wrapper
246const authFetch = async (url: string, options: RequestInit = {}) => {
247 const token = await auth.getToken();
248 
249 return fetch(url, {
250 ...options,
251 headers: {
252 ...options.headers,
253 Authorization: token ? `Bearer ${token}` : "",
254 },
255 });
256};
257```
258 
259**Token refresh**:
260 
261```tsx
262let isRefreshing = false;
263let refreshPromise: Promise<string> | null = null;
264 
265const getValidToken = async (): Promise<string> => {
266 const token = await auth.getToken();
267 
268 if (!token || isTokenExpired(token)) {
269 if (!isRefreshing) {
270 isRefreshing = true;
271 refreshPromise = refreshToken().finally(() => {
272 isRefreshing = false;
273 refreshPromise = null;
274 });
275 }
276 return refreshPromise!;
277 }
278 
279 return token;
280};
281```
282 
283---
284 
285### 5. Offline Support
286 
287Network-status detection with NetInfo and offline-first React Query setup: see [./references/offline-and-cancellation.md](./references/offline-and-cancellation.md).
288 
289---
290 
291### 6. Environment Variables
292 
293**Using environment variables for API configuration**:
294 
295Expo supports environment variables with the `EXPO_PUBLIC_` prefix. These are inlined at build time and available in your JavaScript code.
296 
297```tsx
298// .env
299EXPO_PUBLIC_API_URL=https://api.example.com
300EXPO_PUBLIC_API_VERSION=v1
301 
302// Usage in code
303const API_URL = process.env.EXPO_PUBLIC_API_URL;
304 
305const fetchUsers = async () => {
306 const response = await fetch(`${API_URL}/users`);
307 return response.json();
308};
309```
310 
311**Environment-specific configuration**:
312 
313```tsx
314// .env.development
315EXPO_PUBLIC_API_URL=http://localhost:3000
316 
317// .env.production
318EXPO_PUBLIC_API_URL=https://api.production.com
319```
320 
321**Creating an API client with environment config**:
322 
323```tsx
324// api/client.ts
325const BASE_URL = process.env.EXPO_PUBLIC_API_URL;
326 
327if (!BASE_URL) {
328 throw new Error("EXPO_PUBLIC_API_URL is not defined");
329}
330 
331export const apiClient = {
332 get: async <T,>(path: string): Promise<T> => {
333 const response = await fetch(`${BASE_URL}${path}`);
334 if (!response.ok) throw new Error(`HTTP ${response.status}`);
335 return response.json();
336 },
337 
338 post: async <T,>(path: string, body: unknown): Promise<T> => {
339 const response = await fetch(`${BASE_URL}${path}`, {
340 method: "POST",
341 headers: { "Content-Type": "application/json" },
342 body: JSON.stringify(body),
343 });
344 if (!response.ok) throw new Error(`HTTP ${response.status}`);
345 return response.json();
346 },
347};
348```
349 
350**Important notes**:
351 
352- Only variables prefixed with `EXPO_PUBLIC_` are exposed to the client bundle
353- Never put secrets (API keys with write access, database passwords) in `EXPO_PUBLIC_` variables—they're visible in the built app
354- Environment variables are inlined at **build time**, not runtime
355- Restart the dev server after changing `.env` files
356- For server-side secrets in API routes, use variables without the `EXPO_PUBLIC_` prefix
357 
358**TypeScript support**:
359 
360```tsx
361// types/env.d.ts
362declare global {
363 namespace NodeJS {
364 interface ProcessEnv {
365 EXPO_PUBLIC_API_URL: string;
366 EXPO_PUBLIC_API_VERSION?: string;
367 }
368 }
369}
370 
371export {};
372```
373 
374---
375 
376### 7. Request Cancellation
377 
378AbortController on unmount (React Query cancels automatically): see [./references/offline-and-cancellation.md](./references/offline-and-cancellation.md).
379 
380---
381 
382## Decision Tree
383 
384```
385User asks about networking
386 |-- Route-level data loading (web, SDK 55+)?
387 | \-- Expo Router loaders — see references/expo-router-loaders.md
388 |
389 |-- Basic fetch?
390 | \-- Use fetch API with error handling
391 |
392 |-- Need caching/state management?
393 | |-- Complex app -> React Query (TanStack Query)
394 | \-- Simpler needs -> SWR or custom hooks
395 |
396 |-- Authentication?
397 | |-- Token storage -> expo-secure-store
398 | \-- Token refresh -> Implement refresh flow
399 |
400 |-- Error handling?
401 | |-- Network errors -> Check connectivity first
402 | |-- HTTP errors -> Parse response, throw typed errors
403 | \-- Retries -> Exponential backoff
404 |
405 |-- Offline support?
406 | |-- Check status -> NetInfo
407 | \-- Queue requests -> React Query persistence
408 |
409 |-- Environment/API config?
410 | |-- Client-side URLs -> EXPO_PUBLIC_ prefix in .env
411 | |-- Server secrets -> Non-prefixed env vars (API routes only)
412 | \-- Multiple environments -> .env.development, .env.production
413 |
414 \-- Performance?
415 |-- Caching -> React Query with staleTime
416 |-- Deduplication -> React Query handles this
417 \-- Cancellation -> AbortController or React Query
418```
419 
420## Common Mistakes
421 
422**Wrong: No error handling**
423 
424```tsx
425const data = await fetch(url).then((r) => r.json());
426```
427 
428**Right: Check response status**
429 
430```tsx
431const response = await fetch(url);
432if (!response.ok) throw new Error(`HTTP ${response.status}`);
433const data = await response.json();
434```
435 
436**Wrong: Storing tokens in AsyncStorage**
437 
438```tsx
439await AsyncStorage.setItem("token", token); // Not secure!
440```
441 
442**Right: Use SecureStore for sensitive data**
443 
444```tsx
445await SecureStore.setItemAsync("token", token);
446```
447 
448## Example Invocations
449 
450User: "How do I make API calls in React Native?"
451-> Use fetch, wrap with error handling
452 
453User: "Should I use React Query or SWR?"
454-> React Query for complex apps, SWR for simpler needs
455 
456User: "My app needs to work offline"
457-> Use NetInfo for status, React Query persistence for caching
458 
459User: "How do I handle authentication tokens?"
460-> Store in expo-secure-store, implement refresh flow
461 
462User: "API calls are slow"
463-> Check caching strategy, use React Query staleTime
464User: "How do I configure different API URLs for dev and prod?"
465-> Use `EXPO_PUBLIC_` env vars with .env.development and .env.production files
466User: "Where should I put my API key?"
467-> Client-safe keys: `EXPO_PUBLIC_` in .env. Secret keys: non-prefixed env vars in API routes only
468 
469User: "How do I load data for a page in Expo Router?"
470-> See references/expo-router-loaders.md for route-level loaders (web, SDK 55+). For native, use React Query or fetch.
471 
472## Submitting Feedback
473If you encounter errors, misleading or outdated information in this skill, report it so Expo can improve:
474```bash
475npx --yes submit-expo-feedback@latest --category skills --subject "expo-data-fetching" "<actionable feedback>"
476```
477Only submit when you have something specific and actionable to report. Include as much relevant context as possible.
478If an AI agent repeatedly failed or the user had to take over an Expo task, load the expo-skill-feedback skill and follow its eval-candidate flow instead of reusing the command above.
479 

Discussion

Alternatives

`expo-overview` — router & shared rules for Expo / EASEntry point and router for every Expo or EAS task. Load this skill first — before writing code and before choosing another expo-* / eas-* skill — when the request, PRD, or spec mentions Expo, EAS, Expo Go, or an expo-* package, or the project has an `expo` dependency in `package.json`. Within that gate it also covers app specs and designs to implement (tabs, stacks, maps, lists, navigation, building from a screenshot), and phrasings like 'implement a mobile app', 'make my app look native', 'add navigation', 'fetch some data', 'upgrade my SDK', 'add Expo to my existing native app', 'ship to the App Store', or 'I'm new to Expo, where do I start'. A fully specified request (SDK pinned, libraries named, layout given) still routes through here — the shared setup rules still apply. Do NOT load it when neither signal is present: a bare React Native project with no `expo` dependency is not Expo work. Detects the real goal, routes to the right expo-* / eas-* skill, and owns the shared setup rules.Coding · MITAdd an App Clip to an Expo AppAdd an iOS App Clip target to an Expo app. Use when the user mentions App Clip, AASA, apple-app-site-association, appclips, smart app banner, or wants to ship a lightweight iOS Clip invoked from a URL alongside their parent app.Coding · MITApp Store DeploymentBuild and submit iOS and Android apps with EAS to TestFlight, the App Store, or Google Play. Supports Expo and other React Native projects, plus existing native apps. Use for eas.json setup, release pipelines, signing, app versions and build numbers, store submissions, and listing metadata. For Expo websites and API routes, use eas-hosting; for adding React Native screens to a native app, use expo-brownfield.Coding · MITDesigning with SleekUse when the user wants to design a mobile app or UI screens, when they mention their Sleek (sleek.design) projects, or when implementing Sleek designs in code (HTML, React Native, SwiftUI).Coding · MIT