# Native Data Fetching

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

- Skill: `involvex/native-data-fetching-2` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add involvex/native-data-fetching-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/involvex/native-data-fetching-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: involvex (https://skillmd.com/u/involvex)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/involvex/native-data-fetching-2

---


# 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+)
```

## 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**:

```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 NetInfo from '@react-native-community/netinfo'
import {onlineManager} from '@tanstack/react-query'

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

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

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.

