mcpField
E
expo-data-fetching

expo-data-fetching

github.com/expo開発3.5k28.4k 回インストール今日更新
リポジトリ

expo-data-fetching について

Expo と React Native の開発者向けに、リクエスト、データ取得、キャッシュ、オフライン対応、認証を扱うネットワークスキルです。fetch、React Query、Expo Router のローダーに関する実践的な指針を提供します。

主な機能

  1. 01fetch リクエストとエラー処理の案内
  2. 02React Query と SWR の説明
  3. 03ローディング状態と空状態の設計
  4. 04認証トークンと更新の管理
  5. 05オフライン対応とキャンセルの設定
  6. 06環境変数による API 設定

活用シーン

  1. 01アプリに API を接続
  2. 02データリクエストにキャッシュを追加
  3. 03ネットワークリクエストの障害を調査
  4. 04オフライン対応とトークン管理を実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/expo/skills --skill expo-data-fetching

インストール後、Claude Code で/expo-data-fetchingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
references/
expo-router-loaders.md
offline-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 として指定して報告してください。

コメント