mcpField
N
native-data-fetching

native-data-fetching

github.com/midudev開発7.0k57 回インストール2 週間前に更新
リポジトリ

native-data-fetching について

Expo 開発向けのネットワーキングスキルです。リクエスト、キャッシュ、認証、オフライン対応に加え、Expo Router ローダーの指針も扱います。

主な機能

  1. 01fetch による API リクエスト
  2. 02React Query による取得とキャッシュ
  3. 03エラー処理とリトライ
  4. 04認証トークンの安全な保存
  5. 05オフライン状態とリクエストのキャンセル
  6. 06環境変数による API URL 設定

活用シーン

  1. 01Expo アプリでの API 呼び出し実装
  2. 02クエリキャッシュとデータ更新の設定
  3. 03トークン更新とオフライン対応
  4. 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 を使用してください。

コメント