adynato-mobile-api
React Native/ExpoアプリでAPI連携を行う際に、データ取得、認証、オフライン対応、エラー処理、楽観的更新といった処理を効率的に実装するためのパターンをまとめたSkill。
📜 元の英語説明(参考)
API integration patterns for Adynato mobile apps. Covers data fetching with TanStack Query, authentication flows, offline support, error handling, and optimistic updates in React Native/Expo apps. Use when integrating APIs into mobile applications.
🇯🇵 日本人クリエイター向け解説
React Native/ExpoアプリでAPI連携を行う際に、データ取得、認証、オフライン対応、エラー処理、楽観的更新といった処理を効率的に実装するためのパターンをまとめたSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o adynato-mobile-api.zip https://jpskill.com/download/9754.zip && unzip -o adynato-mobile-api.zip && rm adynato-mobile-api.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/9754.zip -OutFile "$d\adynato-mobile-api.zip"; Expand-Archive "$d\adynato-mobile-api.zip" -DestinationPath $d -Force; ri "$d\adynato-mobile-api.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
adynato-mobile-api.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
adynato-mobile-apiフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
Mobile API Skill
この Skill は、API を Adynato モバイルアプリに統合する際に使用します。
スタック
- データ取得: TanStack Query (React Query)
- HTTP クライアント: Fetch API または Axios
- 認証ストレージ: expo-secure-store
- オフライン: TanStack Query persistence
セットアップ
Query Client の設定
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 分
gcTime: 1000 * 60 * 30, // 30 分 (以前の cacheTime)
retry: 2,
refetchOnWindowFocus: false, // モバイルにはウィンドウフォーカスがない
},
mutations: {
retry: 1,
},
},
})
Provider のセットアップ
// app/_layout.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/query-client'
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
)
}
API クライアント
基本設定
// lib/api.ts
import * as SecureStore from 'expo-secure-store'
const API_URL = process.env.EXPO_PUBLIC_API_URL
interface RequestOptions extends RequestInit {
requireAuth?: boolean
}
export async function api<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<T> {
const { requireAuth = true, ...fetchOptions } = options
const headers: HeadersInit = {
'Content-Type': 'application/json',
...fetchOptions.headers,
}
if (requireAuth) {
const token = await SecureStore.getItemAsync('auth_token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
const response = await fetch(`${API_URL}${endpoint}`, {
...fetchOptions,
headers,
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new ApiError(response.status, error.error || 'Request failed')
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T
}
return response.json()
}
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
this.name = 'ApiError'
}
}
API 関数
// lib/api/users.ts
import { api } from '@/lib/api'
export interface User {
id: string
email: string
name: string
}
export const usersApi = {
getMe: () => api<{ data: User }>('/api/users/me'),
getById: (id: string) => api<{ data: User }>(`/api/users/${id}`),
update: (id: string, data: Partial<User>) =>
api<{ data: User }>(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
}
Query Hooks
基本的な Query
// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query'
import { usersApi } from '@/lib/api/users'
export function useUser(id: string) {
return useQuery({
queryKey: ['users', id],
queryFn: () => usersApi.getById(id),
enabled: !!id,
})
}
Transform を伴う Query
export function useCurrentUser() {
return useQuery({
queryKey: ['users', 'me'],
queryFn: usersApi.getMe,
select: (response) => response.data, // ラッパーからデータを抽出
})
}
ページネーションされた Query
import { useInfiniteQuery } from '@tanstack/react-query'
export function useUsersList() {
return useInfiniteQuery({
queryKey: ['users', 'list'],
queryFn: ({ pageParam = 1 }) =>
api(`/api/users?page=${pageParam}&limit=20`),
getNextPageParam: (lastPage, pages) => {
if (lastPage.data.length < 20) return undefined
return pages.length + 1
},
initialPageParam: 1,
})
}
Mutations
基本的な Mutation
// hooks/useUpdateProfile.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { usersApi } from '@/lib/api/users'
export function useUpdateProfile() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
usersApi.update(id, data),
onSuccess: (response, { id }) => {
// キャッシュを更新
queryClient.setQueryData(['users', id], response)
queryClient.invalidateQueries({ queryKey: ['users', 'me'] })
},
})
}
楽観的アップデート
export function useToggleFavorite() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (itemId: string) => api(`/api/favorites/${itemId}`, {
method: 'POST'
}),
onMutate: async (itemId) => {
// 発信中のリフェッチをキャンセル
await queryClient.cancelQueries({ queryKey: ['items', itemId] })
// 以前の値をスナップショット
const previousItem = queryClient.getQueryData(['items', itemId])
// 楽観的に更新
queryClient.setQueryData(['items', itemId], (old: any) => ({
...old,
isFavorite: !old.isFavorite,
}))
return { previousItem }
},
onError: (err, itemId, context) => {
// エラー時にロールバック
queryClient.setQueryData(['items', itemId], context?.previousItem)
},
onSettled: (data, error, itemId) => {
// 同期を確実にするためにリフェッチ
queryClient.invalidateQueries({ queryKey: ['items', itemId] })
},
})
}
認証フロー
ログイン
// hooks/useAuth.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as SecureStore from 'expo-secure-store'
import { router } from 'expo-router'
import { api } from '@/lib/api'
interface LoginInput {
email: string
password: string
}
export function useLogin() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (input: LoginInput) =>
api<{ token: string; user: User }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify(input),
requireAuth: false,
}),
onSuccess: async (response) => {
await SecureStore.setItemAsync('auth_token', re 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Mobile API Skill
Use this skill when integrating APIs into Adynato mobile apps.
Stack
- Data Fetching: TanStack Query (React Query)
- HTTP Client: Fetch API or Axios
- Auth Storage: expo-secure-store
- Offline: TanStack Query persistence
Setup
Query Client Configuration
// lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes (formerly cacheTime)
retry: 2,
refetchOnWindowFocus: false, // Mobile doesn't have window focus
},
mutations: {
retry: 1,
},
},
})
Provider Setup
// app/_layout.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/query-client'
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
)
}
API Client
Base Configuration
// lib/api.ts
import * as SecureStore from 'expo-secure-store'
const API_URL = process.env.EXPO_PUBLIC_API_URL
interface RequestOptions extends RequestInit {
requireAuth?: boolean
}
export async function api<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<T> {
const { requireAuth = true, ...fetchOptions } = options
const headers: HeadersInit = {
'Content-Type': 'application/json',
...fetchOptions.headers,
}
if (requireAuth) {
const token = await SecureStore.getItemAsync('auth_token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
const response = await fetch(`${API_URL}${endpoint}`, {
...fetchOptions,
headers,
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new ApiError(response.status, error.error || 'Request failed')
}
// Handle 204 No Content
if (response.status === 204) {
return undefined as T
}
return response.json()
}
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
this.name = 'ApiError'
}
}
API Functions
// lib/api/users.ts
import { api } from '@/lib/api'
export interface User {
id: string
email: string
name: string
}
export const usersApi = {
getMe: () => api<{ data: User }>('/api/users/me'),
getById: (id: string) => api<{ data: User }>(`/api/users/${id}`),
update: (id: string, data: Partial<User>) =>
api<{ data: User }>(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
}
Query Hooks
Basic Query
// hooks/useUser.ts
import { useQuery } from '@tanstack/react-query'
import { usersApi } from '@/lib/api/users'
export function useUser(id: string) {
return useQuery({
queryKey: ['users', id],
queryFn: () => usersApi.getById(id),
enabled: !!id,
})
}
Query with Transform
export function useCurrentUser() {
return useQuery({
queryKey: ['users', 'me'],
queryFn: usersApi.getMe,
select: (response) => response.data, // Extract data from wrapper
})
}
Paginated Query
import { useInfiniteQuery } from '@tanstack/react-query'
export function useUsersList() {
return useInfiniteQuery({
queryKey: ['users', 'list'],
queryFn: ({ pageParam = 1 }) =>
api(`/api/users?page=${pageParam}&limit=20`),
getNextPageParam: (lastPage, pages) => {
if (lastPage.data.length < 20) return undefined
return pages.length + 1
},
initialPageParam: 1,
})
}
Mutations
Basic Mutation
// hooks/useUpdateProfile.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { usersApi } from '@/lib/api/users'
export function useUpdateProfile() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<User> }) =>
usersApi.update(id, data),
onSuccess: (response, { id }) => {
// Update cache
queryClient.setQueryData(['users', id], response)
queryClient.invalidateQueries({ queryKey: ['users', 'me'] })
},
})
}
Optimistic Update
export function useToggleFavorite() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (itemId: string) => api(`/api/favorites/${itemId}`, {
method: 'POST'
}),
onMutate: async (itemId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['items', itemId] })
// Snapshot previous value
const previousItem = queryClient.getQueryData(['items', itemId])
// Optimistically update
queryClient.setQueryData(['items', itemId], (old: any) => ({
...old,
isFavorite: !old.isFavorite,
}))
return { previousItem }
},
onError: (err, itemId, context) => {
// Rollback on error
queryClient.setQueryData(['items', itemId], context?.previousItem)
},
onSettled: (data, error, itemId) => {
// Refetch to ensure sync
queryClient.invalidateQueries({ queryKey: ['items', itemId] })
},
})
}
Authentication Flow
Login
// hooks/useAuth.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import * as SecureStore from 'expo-secure-store'
import { router } from 'expo-router'
import { api } from '@/lib/api'
interface LoginInput {
email: string
password: string
}
export function useLogin() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (input: LoginInput) =>
api<{ token: string; user: User }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify(input),
requireAuth: false,
}),
onSuccess: async (response) => {
await SecureStore.setItemAsync('auth_token', response.token)
queryClient.setQueryData(['users', 'me'], { data: response.user })
router.replace('/(tabs)')
},
})
}
export function useLogout() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async () => {
await SecureStore.deleteItemAsync('auth_token')
},
onSuccess: () => {
queryClient.clear()
router.replace('/(auth)/login')
},
})
}
Auth State Check
// hooks/useAuthState.ts
import { useQuery } from '@tanstack/react-query'
import * as SecureStore from 'expo-secure-store'
export function useAuthState() {
return useQuery({
queryKey: ['auth', 'state'],
queryFn: async () => {
const token = await SecureStore.getItemAsync('auth_token')
return { isAuthenticated: !!token }
},
staleTime: Infinity,
})
}
Error Handling
Global Error Handler
// In query client setup
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
onError: (error) => {
if (error instanceof ApiError) {
if (error.status === 401) {
// Handle unauthorized - redirect to login
SecureStore.deleteItemAsync('auth_token')
router.replace('/(auth)/login')
return
}
}
// Show toast or alert
Alert.alert('Error', error.message)
},
},
},
})
Per-Query Error Handling
function ProfileScreen() {
const { data, error, isLoading, refetch } = useCurrentUser()
if (isLoading) return <LoadingSpinner />
if (error) {
return (
<ErrorView
message={error.message}
onRetry={refetch}
/>
)
}
return <ProfileContent user={data} />
}
Offline Support
Query Persistence
// lib/query-client.ts
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { persistQueryClient } from '@tanstack/react-query-persist-client'
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
})
persistQueryClient({
queryClient,
persister: asyncStoragePersister,
})
Network Status
// hooks/useNetworkStatus.ts
import { useEffect, useState } from 'react'
import NetInfo from '@react-native-community/netinfo'
import { onlineManager } from '@tanstack/react-query'
export function useNetworkStatus() {
const [isOnline, setIsOnline] = useState(true)
useEffect(() => {
return NetInfo.addEventListener((state) => {
const online = !!state.isConnected
setIsOnline(online)
onlineManager.setOnline(online)
})
}, [])
return isOnline
}
Usage in Components
// screens/ProfileScreen.tsx
import { useCurrentUser, useUpdateProfile } from '@/hooks/useUser'
export function ProfileScreen() {
const { data: user, isLoading } = useCurrentUser()
const updateProfile = useUpdateProfile()
const handleSave = (formData: Partial<User>) => {
updateProfile.mutate(
{ id: user.id, data: formData },
{
onSuccess: () => {
Alert.alert('Success', 'Profile updated!')
},
}
)
}
if (isLoading) return <LoadingSpinner />
return (
<ProfileForm
user={user}
onSave={handleSave}
isSaving={updateProfile.isPending}
/>
)
}
Checklist
Before shipping:
- [ ] Auth token stored in SecureStore (not AsyncStorage)
- [ ] 401 responses trigger logout/re-auth
- [ ] Loading states shown during fetches
- [ ] Error states with retry options
- [ ] Optimistic updates where appropriate
- [ ] Offline support if required
- [ ] Request timeouts configured
- [ ] No sensitive data logged