Mobile API Architecture
This documentation explains how queries and mutations are handled in the mobile application using React Query, Jotai, and oRPC.
Overview
The mobile application uses a unified approach for API management:
- React Query for server state management.
- Jotai for client state and atomic composition.
- oRPC for type-safe API calls.
- Form Atoms for form state management.
Provider Architecture
Mobile Providers
Mobile provider composition in apps/mobile/src/lib/providers/mobile-providers.tsx:
export const MobileProviders: React.FC<MobileProvidersProps> = ({
children,
}) => {
const { session, isLoaded, isSignedIn } = useSession()
const setAuthToken = useSetAtom(authTokenAtom)
// Sync token when Clerk finishes loading, without blocking render.
useEffect(() => {
if (!isLoaded || !isSignedIn || !session?.getToken) {
return
}
const syncToken = async () => {
try {
const token = await session.getToken()
if (token) {
setAuthToken(token)
}
} catch (error) {
console.warn("Failed to sync auth token:", error)
}
}
syncToken()
}, [session, isLoaded, isSignedIn, setAuthToken])
return (
<StoreAndQueryClientProvider>
<MobileClerkAuthAtomProvider>
<AmplitudeProvider>
<UserAndCoreProviders
shouldHydrateUser={Boolean(isLoaded && isSignedIn)}
currency={getCurrencyStorage(getItem(LOCAL) ?? Locale.English)}
marketplace={getMarketplaceStorage()}
>
{children}
</UserAndCoreProviders>
</AmplitudeProvider>
</MobileClerkAuthAtomProvider>
</StoreAndQueryClientProvider>
)
}Hierarchy is:
StoreAndQueryClientProvider -> MobileClerkAuthAtomProvider -> AmplitudeProvider -> UserAndCoreProviders.
Query Client Configuration
makeQueryClient() in mobile-providers.tsx:
function makeQueryClient() {
return new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
handleApiError(error)
},
}),
defaultOptions: {
queries: {
staleTime: 3 * 60 * 1000,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
throwOnError: true,
retry: (failureCount, error) => {
const isOffline = jotaiStore.get(isDefinitelyOfflineAtom)
if (isOffline) {
return false
}
if (error instanceof Error) {
const lowerMessage = error.message?.toLowerCase()
if (error.message?.includes("UNAUTHORIZED") && failureCount === 0) {
return true
}
if (
lowerMessage?.includes("fetch") ||
lowerMessage?.includes("network") ||
lowerMessage?.includes("connection")
) {
return failureCount < 2
}
}
return false
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
mutations: {
retry: (failureCount, error) => {
const isOffline = jotaiStore.get(isDefinitelyOfflineAtom)
if (isOffline) {
return false
}
if (
error instanceof Error &&
error.message?.includes("UNAUTHORIZED") &&
failureCount === 0
) {
return true
}
return false
},
retryDelay: 1000,
onError: (error) => {
handleApiError(error)
},
},
},
})
}Atomic File Organization
The mobile app follows an atomic splitting strategy. Each feature area organizes atoms into focused files.
File Structure
| File | Purpose | Key Utilities |
|---|---|---|
queries.ts | Data fetching atoms | makeQueryAtoms, makeInfiniteQueryAtoms, atomWithSwr |
mutations.ts | Operation atoms for state changes | atomWithMutation, atomWithDebouncedMutation |
mutations-optimistic.ts | Optimistic update mutations (feature-specific) | Jotai write atoms + query cache updates |
ui.ts | UI state atoms and computed data | atom, derived atoms |
forms.ts | Form field atoms and validation | fieldAtom, form state atoms |
effects.ts | Side effect atoms (replacing many useEffects) | atomEffect, write-only atoms |
types.ts | Feature-specific atom/mutation/query types | Type aliases/interfaces |
Example Directory Structure
apps/mobile/src/
βββ lib/store/shared/
β βββ game/
β βββ user/
β βββ price/
β βββ misc/
βββ app/inventory/_atoms/
β βββ queries.ts
β βββ mutations.ts
β βββ mutations-optimistic.ts
β βββ ui.ts
β βββ forms.ts
β βββ effects.ts
β βββ types.ts
β βββ index.ts
βββ lib/store/utils/
βββ atom-with-swr.ts
βββ atom-with-debounce.ts
βββ debounced-field-atom.tsAPI Patterns
1. Queries with oRPC + Jotai
import { makeQueryAtoms } from "@/lib/react-query"
import { atomWithSwr } from "@/lib/store/utils"
export const [gamesAtom] = makeQueryAtoms(
["game", "all"],
() => null,
(orpc) => () => orpc.game.getGames(),
)
export const gamesSwrAtom = atomWithSwr(gamesAtom)2. Mutations with oRPC + Jotai
export const updateUserPreferencesMutation = atomWithMutation((get) => {
const orpc = get(orpcAtom)
const queryClient = get(queryClientAtom)
return {
mutationKey: ["updateUserPreferences"],
mutationFn: async (payload: UpdateUserPreferencesDTO) => {
return orpc.user.updatePreferences(payload)
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: queries.user._def })
},
}
})3. Debounced Mutations (atomWithDebouncedMutation)
Unlike a simple atomWithMutation, this utility returns a full queue control surface:
addItemAtomflushAtompendingCountAtomhasPendingAtomclearAtommutationAtompendingItemsAtomprocessingItemsAtomisProcessingAtom
Behavior details:
- Default debounce is
400ms. - Queue deduplication is driven by
getItemKey. - Uses
p-limit(1)to serialize batch execution. - Supports
maxBatchSizefor chunked processing. - Supports
onSuccessWithAtomsfor atom-aware post-success updates.
const updateAtoms = atomWithDebouncedMutation((get) => ({
mutationKey: ["inventory", "batch-update"],
mutationFn: async (items) => {
const orpc = get(orpcAtom)
return orpc.inventory.batchUpdate(items)
},
getItemKey: (item) => `${item.productId}-${item.condition}`,
debounceMs: 400,
maxBatchSize: 100,
}))Key Differences from Web
- No SSR: Mobile does not use server-side rendering.
- Provider Composition: Mobile wraps auth + analytics + query/store providers in one tree.
- Auto-Save Integration: Mobile has built-in auto-save patterns for form atoms.
- Error Display: Uses mobile-specific error display and centralized API error handling.
- 3-minute stale time with network-aware retry: Not infinite stale time.
- Offline support: Queries are disabled when offline and auto-refetch on reconnect.
- MMKV-backed atom persistence: Persistent client state uses MMKV via Jotai storage.
Utility Atoms Reference
| Utility | Import | Purpose |
|---|---|---|
atomWithSwr | @/lib/store/utils | SWR-like caching behavior |
makeQueryAtoms | @/lib/react-query | Query atoms with status tracking |
makeInfiniteQueryAtoms | @/lib/react-query | Infinite query atoms with pagination |
atomWithDebouncedMutation | @/lib/store/atom-with-debounced-mutation | Batched debounced mutations with queue |
atomWithMMKVStorage | @/lib/jotaiStorage | MMKV-persisted Jotai atoms |
atomWithMutation | jotai-tanstack-query | Standard mutation atoms |
atomWithQuery | jotai-tanstack-query | Standard query atoms |
atomWithInfiniteQuery | jotai-tanstack-query | Infinite-scroll query atoms |
This architecture provides a unified foundation for server state management in mobile while keeping offline and persistence behavior explicit.