mcpField
D
data-fetching

data-fetching

github.com/gigs-slc開発363 回インストール40 週間前に更新
リポジトリ

data-fetching について

data-fetchingは、APIリクエスト、キャッシュ、認証、オフライン動作を扱うExpo向けネットワークスキルです。fetch、React Query、エラー処理、安全なトークン保存、環境別API設定の実装パターンを示します。

主な機能

  1. 01fetchによるAPIリクエスト
  2. 02React QueryとSWRの指針
  3. 03エラー処理とリトライのパターン
  4. 04安全なトークン保存と更新
  5. 05オフライン状態とキャッシュのパターン
  6. 06環境別のAPI設定

活用シーン

  1. 01ExpoアプリへのAPIリクエスト追加
  2. 02データ取得とキャッシュ方式の選択
  3. 03ネットワークエラーとリトライの調査
  4. 04認証とオフライン動作への対応

インストール

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

$npx skills add https://github.com/gigs-slc/react-native-skills --skill data-fetching

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

詳細

Expo Networking

API リクエスト、データ取得、キャッシュ、ネットワークのデバッグなど、ネットワーク関連の作業では必ずこのスキルを使用してください。

使用する場面

次の場合に、このルーターを使用してください。

  • API リクエストを実装する
  • データ取得(React Query、SWR)を設定する
  • ネットワーク障害をデバッグする
  • キャッシュ戦略を実装する
  • オフラインの状況に対応する
  • 認証/トークン管理を行う
  • 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
  |-- 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 ルート内でのみ使用する、プレフィックスなしの環境変数

コメント