Skip to Content
MobileMobile API Architecture

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

FilePurposeKey Utilities
queries.tsData fetching atomsmakeQueryAtoms, makeInfiniteQueryAtoms, atomWithSwr
mutations.tsOperation atoms for state changesatomWithMutation, atomWithDebouncedMutation
mutations-optimistic.tsOptimistic update mutations (feature-specific)Jotai write atoms + query cache updates
ui.tsUI state atoms and computed dataatom, derived atoms
forms.tsForm field atoms and validationfieldAtom, form state atoms
effects.tsSide effect atoms (replacing many useEffects)atomEffect, write-only atoms
types.tsFeature-specific atom/mutation/query typesType 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.ts

API 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:

  • addItemAtom
  • flushAtom
  • pendingCountAtom
  • hasPendingAtom
  • clearAtom
  • mutationAtom
  • pendingItemsAtom
  • processingItemsAtom
  • isProcessingAtom

Behavior details:

  • Default debounce is 400ms.
  • Queue deduplication is driven by getItemKey.
  • Uses p-limit(1) to serialize batch execution.
  • Supports maxBatchSize for chunked processing.
  • Supports onSuccessWithAtoms for 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

  1. No SSR: Mobile does not use server-side rendering.
  2. Provider Composition: Mobile wraps auth + analytics + query/store providers in one tree.
  3. Auto-Save Integration: Mobile has built-in auto-save patterns for form atoms.
  4. Error Display: Uses mobile-specific error display and centralized API error handling.
  5. 3-minute stale time with network-aware retry: Not infinite stale time.
  6. Offline support: Queries are disabled when offline and auto-refetch on reconnect.
  7. MMKV-backed atom persistence: Persistent client state uses MMKV via Jotai storage.

Utility Atoms Reference

UtilityImportPurpose
atomWithSwr@/lib/store/utilsSWR-like caching behavior
makeQueryAtoms@/lib/react-queryQuery atoms with status tracking
makeInfiniteQueryAtoms@/lib/react-queryInfinite query atoms with pagination
atomWithDebouncedMutation@/lib/store/atom-with-debounced-mutationBatched debounced mutations with queue
atomWithMMKVStorage@/lib/jotaiStorageMMKV-persisted Jotai atoms
atomWithMutationjotai-tanstack-queryStandard mutation atoms
atomWithQueryjotai-tanstack-queryStandard query atoms
atomWithInfiniteQueryjotai-tanstack-queryInfinite-scroll query atoms

This architecture provides a unified foundation for server state management in mobile while keeping offline and persistence behavior explicit.

Last updated on