<< All versions
Skill v1.0.0
currentLLM-judged scan90/100nghyane/vstep/mobile-data-patterns
──Details
PublishedOctober 23, 2026 at 07:28 AM
Content Hashsha256:d3ff30a1be8568ff...
Git SHA40c60a5bf2a5
──Files
Files (1 file, 3.7 KB)
SKILL.md3.7 KBactive
SKILL.md · 133 lines · 3.7 KB
version: "1.0.0" name: mobile-data-patterns description: "TanStack Query + Zustand + API integration patterns. Server state, mutations, local stores, error handling, token refresh. Load when working with data layer."
Mobile Data Patterns
QueryClient Setup
ts
// src/lib/query-client.tsconst queryClient = new QueryClient({defaultOptions: {queries: { staleTime: 1000 * 60 * 5, retry: false },mutations: { onError: globalErrorHandler },},});queryClient.getQueryCache().config.onError = globalErrorHandler;queryClient.getMutationCache().config.onError = globalErrorHandler;
- Global error handler: 401 → logout, else → toast/Alert
staleTime: 5 minutes— reasonable for mobileretry: false— fail fast on mobile, user can retry manually
API Client
ts
// src/lib/api.tsimport { getAccessToken } from "./auth";const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "https://api.vstepgo.com";export const api = {get: <T>(path: string) => request<T>("GET", path),post: <T>(path: string, body?: unknown) => request<T>("POST", path, body),put: <T>(path: string, body?: unknown) => request<T>("PUT", path, body),patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body),delete: <T>(path: string) => request<T>("DELETE", path),};
- Key transforms: Laravel snake_case ↔ React Native camelCase
- Auto-unwrap
{ data: T }response wrapper - Auto-refresh 401 tokens via
tryRefresh()
Query Patterns
ts
// src/hooks/use-exams.tsexport function useExams() {return useQuery({queryKey: ["exams"],queryFn: () => api.get<Exam[]>("api/v1/exams"),});}
queryKeyformat:["feature", id?]— flat arraysselectoption for derived dataenabledflag when dependency not ready
Mutation Patterns
ts
// src/hooks/use-auth.tsexport function useLogin() {return useMutation({mutationFn: ({ email, password }) => loginApi(email, password),onSuccess: (data) => {// side effects here},onError: (error) => {// toast/Alert},});}
- All API writes through
useMutation— global onError catches 401 - Components call
mutation.mutate()— no try/catch onSuccessfor side effects (navigation, state update)
Zustand Local Stores
ts
// src/features/coin/coin-store.tsimport { create } from "zustand";interface CoinStore {amount: number;setAmount: (n: number) => void;addCoins: (n: number) => void;}export const useCoinStore = create<CoinStore>((set) => ({amount: 0,setAmount: (n) => set({ amount: n }),addCoins: (n) => set((s) => ({ amount: s.amount + n })),}));
- Use Zustand for: coin, streak, notification (local state only)
- Sync with backend on app launch via
loadXxx()functions - No async in store — async in
loadXxx()functions inapi.ts
Auth Flow
ts
// Token storage: expo-secure-store (not AsyncStorage)await SecureStore.setItemAsync("vstep_access_token", token);await SecureStore.setItemAsync("vstep_user", JSON.stringify(user));
- Login → save tokens → update auth state
- App launch → restore from SecureStore → refresh token
- 401 →
tryRefresh()→ if fail → logout → redirect login - Logout → clear SecureStore → clear QueryClient cache
Error Handling
- Never try/catch in components for toast
- Global QueryClient onError handles 401 → logout
- Use React Native
Alert.alert()for user-facing errors - Log errors for debugging, but don't expose raw errors to UI
Data Rules
- Backend response:
{ data: T }→ApiResponse<T> - Null handling:
{value && <Component />}— don't hide null with?? fallback - Types defined in
src/types/api.ts— don't inline.json<{...}>