native-data-fetching について
Expo 開発向けのネットワーキングスキルです。リクエスト、キャッシュ、認証、オフライン対応に加え、Expo Router ローダーの指針も扱います。
主な機能
- 01fetch による API リクエスト
- 02React Query による取得とキャッシュ
- 03エラー処理とリトライ
- 04認証トークンの安全な保存
- 05オフライン状態とリクエストのキャンセル
- 06環境変数による API URL 設定
活用シーン
- 01Expo アプリでの API 呼び出し実装
- 02クエリキャッシュとデータ更新の設定
- 03トークン更新とオフライン対応
- 04ネットワークエラーの調査や環境別 API URL の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ midudev/ autoskills --skill native-data-fetchingインストール後、Claude Code で/native-data-fetchingを入力して読み込まれたか確認します。
同梱ファイルreferences/expo-router-loaders.md
詳細
Expo ネットワーキング
API リクエスト、データ取得、キャッシュ、ネットワークのデバッグなど、あらゆるネットワーク関連の作業では、必ずこのスキルを使用してください。
参考資料
必要に応じて、以下のリソースを参照してください。
references/
expo-router-loaders.md Route-level data loading with Expo Router loaders (web, SDK 55+)
使用する場面
次の作業を行うときに、このスキルを使用してください。
- API リクエストの実装
- データ取得(React Query、SWR)のセットアップ
- Expo Router のデータローダー(
useLoaderData、Web SDK 55 以降)の使用 - ネットワーク障害のデバッグ
- キャッシュ戦略の実装
- オフライン時の対応
- 認証/トークン管理
- API URL と環境変数の設定
推奨事項
- axios は避け、expo/fetch を優先してください
よくある問題と解決策
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, 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} />;
}
ミューテーション:
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. エラー処理
包括的なエラー処理:
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. オフライン対応
ネットワーク状態の確認:
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;
}
React Query によるオフラインファーストの実装:
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. 環境変数
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. リクエストのキャンセル
アンマウント時にキャンセル:
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]);
React Query を使用する場合(自動):
// React Query automatically cancels requests when queries are invalidated
// or components unmount
判断フロー
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 ファイルで EXPOPUBLIC 環境変数を使用してください
ユーザー:「API キーはどこに置けばよいですか?」 -> クライアントで安全に使えるキー:.env 内の EXPOPUBLIC。シークレットキー:API ルートでのみ、プレフィックスのない環境変数を使用してください
ユーザー:「Expo Router でページのデータを読み込むにはどうすればよいですか?」 -> ルートレベルのローダーについては references/expo-router-loaders.md を参照してください(Web、SDK 55 以降)。ネイティブでは React Query または fetch を使用してください。