This commit is contained in:
8
native-data-fetching/.skillshare-meta.json
Normal file
8
native-data-fetching/.skillshare-meta.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"source": "github.com/expo/skills/tree/main/plugins/expo-app-design/skills/native-data-fetching",
|
||||
"type": "github-subdir",
|
||||
"installed_at": "2026-01-30T02:26:56.038194184Z",
|
||||
"repo_url": "https://github.com/expo/skills.git",
|
||||
"subdir": "plugins/expo-app-design/skills/native-data-fetching",
|
||||
"version": "b631a60"
|
||||
}
|
||||
491
native-data-fetching/SKILL.md
Normal file
491
native-data-fetching/SKILL.md
Normal file
@@ -0,0 +1,491 @@
|
||||
---
|
||||
name: native-data-fetching
|
||||
description: 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.
|
||||
version: 1.0.0
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# 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**:
|
||||
|
||||
```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();
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
**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));
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Authentication
|
||||
|
||||
**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;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Offline Support
|
||||
|
||||
**Check network status**:
|
||||
|
||||
```tsx
|
||||
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**:
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```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 {};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Request Cancellation
|
||||
|
||||
**Cancel on unmount**:
|
||||
|
||||
```tsx
|
||||
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):
|
||||
|
||||
```tsx
|
||||
// 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**
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user