expo-data-fetching について
Expo と React Native の開発者向けに、リクエスト、データ取得、キャッシュ、オフライン対応、認証を扱うネットワークスキルです。fetch、React Query、Expo Router のローダーに関する実践的な指針を提供します。
主な機能
- 01fetch リクエストとエラー処理の案内
- 02React Query と SWR の説明
- 03ローディング状態と空状態の設計
- 04認証トークンと更新の管理
- 05オフライン対応とキャンセルの設定
- 06環境変数による API 設定
活用シーン
- 01アプリに API を接続
- 02データリクエストにキャッシュを追加
- 03ネットワークリクエストの障害を調査
- 04オフライン対応とトークン管理を実装
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ expo/ skills --skill expo-data-fetchingインストール後、Claude Code で/expo-data-fetchingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/expo-router-loaders.mdoffline-and-cancellation.md詳細
Expo ネットワーキング
API リクエスト、データ取得、キャッシュ、ネットワークのデバッグなど、あらゆるネットワーク関連の作業で、このスキルを必ず使用してください。
参考資料
必要に応じて、次のリソースを参照してください。
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
使用する場面
次のような場合に、このスキルを使用してください。
- API リクエストを実装するとき
- データ取得(React Query、SWR)を設定するとき
- Expo Router のデータローダー(
useLoaderData、web SDK 55+)を使用するとき - ネットワーク障害をデバッグするとき
- キャッシュ戦略を実装するとき
- オフラインの状況に対応するとき
- 認証/トークン管理を行うとき
- API URL と環境変数を設定するとき
推奨事項
- axios は避け、expo/fetch を優先してください
すべての画面に必要な4つの状態
データを読み込む画面では、読み込み中、エラー、空、コンテンツの各状態を設計してください。これらは同時に発生する場合があります。たとえば、更新時のエラーがキャッシュ済みコンテンツと共存することがあります。
- 読み込み中 ≠ 空。 空とは、データがないことではなく、取得が完了し、項目が0件であることを意味します。リストの長さを確認する前に、初回読み込み、失敗、ハイドレーションを処理してください。TanStack Query v5 では、
isLoadingは初回のフェッチが実行中であることを意味します。無効化されたクエリやオフラインで一時停止されたクエリは、読み込み中でなくてもデータがない場合があります。その場合は、前提条件またはオフライン状態を表示してください。 - 空の状態は設計された状態であり、空白のリストではありません。 FlatList/FlashList では
ListEmptyComponentを使い、空である理由を説明して、関連する次のアクションを提示してください。「項目はまだありません」なら作成を促し、「検索結果がありません」なら検索/フィルターの変更またはクリアを促します。 - 再取得中も古いコンテンツを保持します。 更新に失敗してもキャッシュされた
dataを表示し、ユーザーの操作を妨げないエラーと再試行手段を提示してください。初回フェッチのスピナーにはisLoading、バックグラウンドでの処理にはisFetchingを使用してください。レイアウトがわかっている場合、時間のかかる初回読み込みにはスケルトンを優先し、ユーザーが開始した更新にはRefreshControlを使用してください。 - ハイドレーションを待ってから処理します。 初期 UI やリダイレクトが永続化された状態(認証トークン、オンボーディングフラグ)に依存する場合、その状態が読み込まれるまでルートレイアウトは何も表示しないか、スプラッシュを表示します。ハイドレーション前の状態で判断すると、コールドスタートのたびに誤った画面が一瞬表示され、ハイドレーション前に届いたディープリンクも誤った場所に誘導されます。
保存時は作業内容を保持します。 ミューテーションが保留中の間は、連続送信を無効にしてください。失敗した場合は下書きを保持し、インラインエラーを表示して、ユーザーが再試行できるようにします。クリアまたは閉じるのは成功後に限ります。楽観的更新を行う場合は、失敗時に以前の値に戻すか、編集が未同期であることを示してください。保存の失敗後に再試行する流れを検証してください。
よくある問題と解決策
1. 基本的な Fetch の使い方
シンプルな 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();
};
2. React Query(TanStack Query)
セットアップ:
// 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>
);
}
データの取得:
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} />}
</>
);
}
ミューテーション:
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. エラー処理
包括的なエラー処理:
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");
}
};
再試行ロジック:
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. 認証
トークン管理:
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}` : "",
},
});
};
トークンのリフレッシュ:
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. オフラインサポート
NetInfo によるネットワーク状態の検出と、オフラインファーストの React Query 設定については、./references/offline-and-cancellation.md を参照してください。
6. 環境変数
API 設定での環境変数の使用:
Expo は EXPO_PUBLIC_ プレフィックス付きの環境変数をサポートしています。これらはビルド時にコードへインライン化され、JavaScript コードで使用できます。
// .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();
};
環境ごとの設定:
// .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_がプレフィックスに付いた変数のみ、クライアントバンドルに公開されます- シークレット(書き込み権限のある API キー、データベースのパスワード)を
EXPO_PUBLIC_変数に設定しないでください。ビルド済みアプリから確認できます - 環境変数は実行時ではなく、ビルド時にインライン化されます
.envファイルを変更した後は、開発サーバーを再起動してください- API ルートでサーバー側のシークレットを使う場合は、
EXPO_PUBLIC_プレフィックスのない変数を使用してください
TypeScript のサポート:
// types/env.d.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
EXPO_PUBLIC_API_URL: string;
EXPO_PUBLIC_API_VERSION?: string;
}
}
}
export {};
7. リクエストのキャンセル
アンマウント時の AbortController(React Query は自動的にキャンセルします):./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
よくある間違い
誤り:エラー処理をしない
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); // Not secure!
正解:機密データには 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 を設定するにはどうすればよいですか?」
-> .env.development と .env.production ファイルで EXPO_PUBLIC_ 環境変数を使用します
ユーザー:「API キーはどこに置けばよいですか?」
-> クライアントで安全に使えるキー:.env 内の EXPO_PUBLIC_。シークレットキー:プレフィックスなしの環境変数として API ルート内でのみ使用します
ユーザー:「Expo Router でページのデータを読み込むにはどうすればよいですか?」 -> ルートレベルのローダーについては references/expo-router-loaders.md を参照してください(Web、SDK 55+)。ネイティブでは React Query または fetch を使用します。
フィードバックの送信
このスキルに誤り、誤解を招く情報、または古い情報がある場合は、Expo の改善のために報告してください:
npx --yes @expo/agent-cli@latest feedback --category skills --subject "expo-data-fetching" "<actionable feedback>"
具体的で実行可能な報告内容がある場合にのみ送信してください。関連するコンテキストをできるだけ詳しく含めてください。
エージェントが Expo のタスクに何度も失敗する場合や、ユーザーが介入しなければならない場合は、タスクの内容、期待される結果、問題が起きた箇所を簡潔にまとめ、--category evals と関連する Expo パッケージまたはコマンドを --subject として指定して報告してください。