From harness-claude
Handles network requests, offline support, caching, retries, and connectivity monitoring in React Native using TanStack Query and NetInfo.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Handle network requests, offline support, caching, and connectivity monitoring in React Native
Guides data fetching in React Native/Expo apps using Fetch API and TanStack Query, covering API calls, caching, mutations, authentication tokens, offline support, and request cancellation.
Implements and debugs network requests, API calls, data fetching using fetch, React Query, SWR, with caching, error handling, offline support, and Expo Router loaders.
Implements and debugs network requests in Expo apps using fetch, React Query, SWR, Expo Router loaders, caching, and offline handling.
Share bugs, ideas, or general feedback.
Handle network requests, offline support, caching, and connectivity monitoring in React Native
npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes (formerly cacheTime)
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
},
},
});
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Navigation />
</QueryClientProvider>
);
}
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useOrders() {
return useQuery({
queryKey: ['orders'],
queryFn: async (): Promise<Order[]> => {
const response = await fetch(`${API_URL}/orders`);
if (!response.ok) throw new Error('Failed to fetch orders');
return response.json();
},
});
}
function useCreateOrder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreateOrderInput) => {
const response = await fetch(`${API_URL}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
if (!response.ok) throw new Error('Failed to create order');
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['orders'] });
},
});
}
@react-native-community/netinfo.npx expo install @react-native-community/netinfo
import NetInfo from '@react-native-community/netinfo';
import { onlineManager } from '@tanstack/react-query';
// Integrate with TanStack Query — pauses queries when offline
onlineManager.setEventListener((setOnline) => {
return NetInfo.addEventListener((state) => {
setOnline(!!state.isConnected);
});
});
// Use in components
function useNetworkStatus() {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
return NetInfo.addEventListener((state) => {
setIsConnected(!!state.isConnected);
});
}, []);
return isConnected;
}
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
key: 'REACT_QUERY_CACHE',
});
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister, maxAge: 24 * 60 * 60 * 1000 }}
>
<Navigation />
</PersistQueryClientProvider>
);
}
function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (productId: string) =>
fetch(`${API_URL}/favorites/${productId}`, { method: 'POST' }),
onMutate: async (productId) => {
await queryClient.cancelQueries({ queryKey: ['product', productId] });
const previous = queryClient.getQueryData<Product>(['product', productId]);
queryClient.setQueryData<Product>(['product', productId], (old) =>
old ? { ...old, isFavorite: !old.isFavorite } : old
);
return { previous };
},
onError: (_err, productId, context) => {
queryClient.setQueryData(['product', productId], context?.previous);
},
onSettled: (_data, _err, productId) => {
queryClient.invalidateQueries({ queryKey: ['product', productId] });
},
});
}
class ApiClient {
private baseUrl: string;
private getToken: () => Promise<string | null>;
constructor(baseUrl: string, getToken: () => Promise<string | null>) {
this.baseUrl = baseUrl;
this.getToken = getToken;
}
async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = await this.getToken();
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
});
if (response.status === 401) {
// Trigger token refresh or logout
throw new AuthError('Session expired');
}
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.message ?? 'Request failed');
}
return response.json();
}
}
function OfflineBanner() {
const isConnected = useNetworkStatus();
if (isConnected) return null;
return (
<View style={styles.banner}>
<Text style={styles.bannerText}>You are offline. Some features may be limited.</Text>
</View>
);
}
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeout = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
Offline strategy spectrum:
TanStack Query network modes:
online (default): Queries pause when offline, resume when onlinealways: Queries run regardless of connectivity (for local data sources)offlineFirst: Try cache first, then networkRetry strategies: Use exponential backoff with jitter for retries. Mobile networks are unreliable — aggressive retrying wastes battery and data. Default to 3 retries with 1s, 2s, 4s delays.
Common mistakes:
response.ok)https://tanstack.com/query/latest