フレームワーク(オープンソースソフトウェア)。ネットワークリクエスト、API呼び出し、またはデータ取得の実装やデバッグを行うあらゆる場面で使用します。 fetch API、React Query、SWR、エラーハンドリング(エラー処理)、キャッシング(データの一時保存)、オフラインサポート、および Expo Router のデータローダー(`useLoaderData`)に対応しています。
Framework (OSS). 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`).
ネットワーク機能を使用するすべての作業(APIリクエスト、データ取得、キャッシング、ネットワーク デバッグなど)には、このスキルを必ず使用してください。
必要に応じて以下を参照してください:
references/
expo-router-loaders.md Expo Routerのローダーを使用したルートレベルのデータ読み込み
(Web、SDK 55以上)
offline-and-cancellation.md NetInfoによるネットワーク状態確認、オフラインファーストの
React Query、AbortController
次のような場合に使用:
useLoaderData、Web SDK 55以上)シンプルなGETリクエスト:
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リクエスト:
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();
};
セットアップ:
// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分間
retry: 2,
},
},
});
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
);
}
データの取得:
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: () => {
// キャッシュを無効化して再取得
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
const handleSubmit = (data: UserData) => {
mutation.mutate(data);
};
return <Form onSubmit={handleSubmit} isLoading={mutation.isPending} />;
}
包括的なエラー処理:
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;
}
// ネットワークエラー(インターネット接続なし、タイムアウトなど)
throw new ApiError("Network error", 0, "NETWORK_ERROR");
}
};
リトライロジック:
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;
// 指数バックオフ(待機時間を段階的に延長)
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
}
};
トークン管理:
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),
};
// 認証付きfetchのラッパー
const authFetch = async (url: string, options: RequestInit = {}) => {
const token = await auth.getToken();
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: token ? `Bearer ${token}` : "",
},
});
};
トークンの更新:
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;
};
NetInfoを使ったネットワーク状態検出とオフラインファーストのReact Query設定については、./references/offline-and-cancellation.md を参照してください。
API設定に環境変数を使用する:
Expoは EXPO_PUBLIC_ プレフィックス付きの環境変数をサポートしています。これらはビルド時にコードに組み込まれ、JavaScriptコードから利用できます。
// .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_API_VERSION=v1
// コードでの使用
const API_URL = process.env.EXPO_PUBLIC_API_URL;
const fetchUsers = async () => {
const response = await fetch(`${API_URL}/users`);
return response.json();
};
環境別の設定:
// .env.development
EXPO_PUBLIC_API_URL=http://localhost:3000
// .env.production
EXPO_PUBLIC_API_URL=https://api.production.com
環境設定でAPIクライアントを作成する:
// 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();
},
};
重要な注意点:
EXPO_PUBLIC_ プレフィックス付きの変数のみがクライアントバンドルに公開されますEXPO_PUBLIC_ 変数に入れないでください。ビルド済みアプリに見えてしまいます.env ファイルを変更した後は、開発サーバーを再起動してくださいEXPO_PUBLIC_ プレフィックスなしの変数を使用してくださいTypeScriptでの型定義:
// types/env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
EXPO_PUBLIC_API_URL: string;
EXPO_PUBLIC_API_VERSION?: string;
}
}
}
export {};
React Queryが自動的にキャンセルするため、マウント解除時にAbortControllerを使用してください。詳細は ./references/offline-and-cancellation.md を参照してください。
ネットワーク関連の質問か
|-- ルートレベルのデータ読み込み(Web、SDK 55以上)?
| \-- Expo Routerのローダー → references/expo-router-loaders.md を参照
|
|-- 基本的なfetch?
| \-- エラーハンドリング付きのfetch APIを使用
|
|-- キャッシング・状態管理が必要?
| |-- 複雑なアプリ → React Query(TanStack Query)
| \-- シンプルな要件 → SWRまたはカスタムフック
|
|-- 認証が必要?
| |-- トークン保存 → expo-secure-store
| \-- トークン更新 → リフレッシュフロー(更新手順)を実装
|
|-- エラーハンドリング?
| |-- ネットワークエラー → 接続確認から開始
| |-- HTTPエラー → レスポンスを解析して型付きエラーを送出
| \-- リトライ → 指数バックオフ(待機時間を段階的に延長)
|
|-- オフラインサポート?
| |-- 状態確認 → NetInfo
| \-- リクエストキュー → React Queryの永続化
|
|-- 環境・API設定?
| |-- クライアント側URL → .envで EXPO_PUBLIC_ プレフィックス
| |-- サーバーシークレット → APIルートのプレフィックスなし変数
| \-- 複数環境 → .env.development、.env.production
|
\-- パフォーマンス?
|-- キャッシング → React Queryで staleTime を設定
|-- 重複削除 → React Queryが自動処理
\-- キャンセル → AbortControllerまたはReact Query
間違った例:エラーハンドリングなし
const data = await fetch(url).then((r) => r.json());
正しい例:レスポンスステータスを確認
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
間違った例:AsyncStorageにトークンを保存
await AsyncStorage.setItem("token", token); // セキュアではありません!
正しい例:機密データはSecureStoreを使用
await SecureStore.setItemAsync("token", token);
ユーザー: 「React NativeでAPIを呼び出すにはどうすればいいですか?」 → fetchを使用し、エラーハンドリングでラップする
ユーザー: 「React QueryとSWRどちらを使うべき?」 → 複雑なアプリはReact Query、シンプルな要件ならSWR
ユーザー: 「アプリがオフラインで動作する必要があります」 → NetInfoで状態確認、React Queryの永続化でキャッシング
ユーザー: 「認証トークンをどう処理するか?」 → expo-secure-storeに保存、リフレッシュフロー(更新手順)を実装
ユーザー: 「APIの呼び出しが遅いです」 → キャッシング戦略を確認、React Queryで staleTime を設定
ユーザー: 「開発環境と本番環境で別のAPI URLを設定するには?」
→ EXPO_PUBLIC_ プレフィックス付き環境変数で .env.development と .env.production ファイルを使用
ユーザー: 「APIキーはどこに置くべき?」
→ クライアント可能なキー: .envで EXPO_PUBLIC_。シークレットキー: APIルート内のプレフィックスなし変数のみ
ユーザー: 「Expo Routerでページのデータを読み込むには?」 → ルートレベルのローダーについては references/expo-router-loaders.md を参照(Web、SDK 55以上)。ネイティブはReact Queryまたはfetchを使用
You MUST use this skill for ANY networking work including API requests, data fetching, caching, or network debugging.
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
Use this skill when:
useLoaderData, web SDK 55+)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();
};
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} />;
}
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));
}
}
};
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;
};
Network-status detection with NetInfo and offline-first React Query setup: see ./references/offline-and-cancellation.md.
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:
EXPO_PUBLIC_ are exposed to the client bundleEXPO_PUBLIC_ variables—they're visible in the built app.env filesEXPO_PUBLIC_ prefixTypeScript support:
// types/env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
EXPO_PUBLIC_API_URL: string;
EXPO_PUBLIC_API_VERSION?: string;
}
}
}
export {};
AbortController on unmount (React Query cancels automatically): see ./references/offline-and-cancellation.md.
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
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);
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.
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.
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。