Skip to content

Enterprise grade patterns

Here’s how to implement enterprise-grade patterns in an Expo + Bun app using TanStack Query + Zustand + NativeWind.

This is the stuff that separates mid-level apps from production SaaS/mobile systems.


App works even:

  • No internet
  • Flaky network
  • Background sync

  • Cache server data (React Query)
  • Store pending mutations locally
  • Sync when online

type QueueItem = {
id: string;
url: string;
method: 'POST' | 'PUT' | 'DELETE';
body: any;
};
let queue: QueueItem[] = [];
export const addToQueue = (item: QueueItem) => {
queue.push(item);
};
export const processQueue = async (apiClient: any) => {
for (const item of queue) {
try {
await apiClient({
url: item.url,
method: item.method,
data: item.body,
});
} catch (e) {
return; // stop if still offline
}
}
queue = [];
};

import NetInfo from '@react-native-community/netinfo';
import { useEffect } from 'react';
import { processQueue } from '@/services/offline/queue';
import { apiClient } from '@/services/api/client';
export const useOnlineSync = () => {
useEffect(() => {
const unsubscribe = NetInfo.addEventListener((state) => {
if (state.isConnected) {
processQueue(apiClient);
}
});
return unsubscribe;
}, []);
};

πŸ‘‰ Call this once in root layout.


import { addToQueue } from '@/services/offline/queue';
const mutation = useMutation({
mutationFn: async (data) => {
try {
return await apiClient.post('/todos', data);
} catch {
addToQueue({
id: Date.now().toString(),
url: '/todos',
method: 'POST',
body: data,
});
}
},
});

import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
cacheTime: 1000 * 60 * 60,
retry: 2,
refetchOnReconnect: true,
refetchOnWindowFocus: false,
},
},
});

Use storage (MMKV recommended):

import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import AsyncStorage from '@react-native-async-storage/async-storage';
persistQueryClient({
queryClient,
persister: createAsyncStoragePersister({
storage: AsyncStorage,
}),
});

queryClient.invalidateQueries({ queryKey: ['user'] });

πŸ‘‰ Always invalidate after mutations.


Terminal window
bun add react-error-boundary

import { ErrorBoundary } from 'react-error-boundary';
import { View, Text, Button } from 'react-native';
function Fallback({ error, resetErrorBoundary }: any) {
return (
<View className="flex-1 items-center justify-center">
<Text>Something went wrong</Text>
<Button title="Retry" onPress={resetErrorBoundary} />
</View>
);
}
export const AppErrorBoundary = ({ children }: any) => (
<ErrorBoundary FallbackComponent={Fallback}>
{children}
</ErrorBoundary>
);

<AppErrorBoundary>
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
</AppErrorBoundary>

  • API failures
  • App crashes
  • User actions (important flows)
  • Performance

export const logger = {
log: (...args: any[]) => {
console.log('[LOG]', ...args);
},
error: (error: any) => {
console.error('[ERROR]', error);
// send to Sentry/Datadog later
},
};

apiClient.interceptors.response.use(
(res) => res,
(error) => {
logger.error({
url: error.config?.url,
message: error.message,
});
return Promise.reject(error);
}
);

ToolPurpose
SentryCrash reporting
DatadogLogs + APM
Firebase AnalyticsUser behavior

React Query already supports retry:

useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3,
retryDelay: (attempt) => attempt * 1000,
});

UI (app/)
↓
Hooks (features/*/hooks.ts)
↓
API (features/*/api.ts)
↓
Service (api client)

import * as SecureStore from 'expo-secure-store';
export const storage = {
set: (key: string, value: string) =>
SecureStore.setItemAsync(key, value),
get: (key: string) =>
SecureStore.getItemAsync(key),
};

  1. User opens app β†’ cached data loads instantly
  2. API refetch runs in background
  3. Offline β†’ mutations queued
  4. Online β†’ auto sync
  5. Errors β†’ caught by boundary + logged
  6. Logs β†’ sent to monitoring

❌ Not persisting cache β†’ bad UX offline ❌ No retry logic β†’ fragile app ❌ Logging only console β†’ useless in prod ❌ No queue β†’ data loss offline ❌ Overusing Zustand for server data


  • React Query = server state
  • Zustand = client/UI state
  • Offline queue = reliability layer
  • Error boundary = safety net
  • Logging = visibility

If you want next level (real enterprise stuff), I can show you:

  • πŸ”₯ Conflict resolution (offline edits vs server truth)
  • ⚑ Background sync with Expo Task Manager
  • 🧠 Event-driven architecture inside React Native
  • πŸš€ Full CI/CD + monitoring pipeline (Sentry + EAS + GitHub Actions)

Just tell me πŸ‘