Part 1: The Modern React Native Tech Stack (2026)
Choosing the right tools before you start saves weeks of refactoring later. Here's what the best teams are using in 2026.
Framework: Expo vs Bare React Native
| Factor | Expo (Recommended) | Bare React Native |
|---|---|---|
| Setup Time | 5 minutes | 30+ minutes |
| Native Modules | Expo SDK + dev builds | Full access |
| OTA Updates | Built-in | Manual setup |
| Build Service | EAS (cloud builds) | Local or custom CI |
| Learning Curve | Gentle | Steep |
Verdict: Start with Expo. You can eject later if needed (but you probably won't).
Current Version: Expo SDK 55 (December 2024)
Quick Start
Terminal
# Create new Expo project
npx create-expo-app@latest my-app
cd my-app
# Start development
npx expo startOfficial Docs: docs.expo.dev↗
Navigation: Expo Router
| Factor | Expo Router (Recommended) | React Navigation |
|---|---|---|
| Setup | Zero config | Manual setup |
| Routing | File-based (like Next.js) | Component-based |
| Deep Linking | Automatic | Manual config |
| Type Safety | Built-in typed routes | Manual typing |
File Structure
app/
├── index.tsx → "/"
├── login.tsx → "/login"
├── (tabs)/
│ ├── index.tsx → "/tabs"
│ ├── profile.tsx → "/tabs/profile"
│ └── settings.tsx → "/tabs/settings"
└── [id].tsx → "/123" (dynamic)Setup
npx expo install expo-router expo-linking expo-constantsapp/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}Navigation in components
import { router } from 'expo-router';
// Navigate
router.push('/profile');
router.replace('/login');
router.back();
// With params
router.push({ pathname: '/user/[id]', params: { id: '123' } });State Management: Zustand + TanStack Query
| Solution | Best For | Bundle Size |
|---|---|---|
| Zustand | Most apps | 1.1 kB |
| Redux Toolkit | Large teams | 11 kB |
| TanStack Query | Server state | 11 kB |
Verdict: Zustand for client state + TanStack Query for server state.
Zustand Setup
npm install zustandstores/useAuthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface AuthState {
user: User | null;
isAuthenticated: boolean;
setUser: (user: User | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
setUser: (user) => set({ user, isAuthenticated: !!user }),
logout: () => set({ user: null, isAuthenticated: false }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
// Usage in components
const { user, isAuthenticated, logout } = useAuthStore();TanStack Query Setup
npm install @tanstack/react-queryhooks/useGenerations.ts
import { useQuery, useMutation } from '@tanstack/react-query';
function useGenerations() {
return useQuery({
queryKey: ['generations'],
queryFn: () => fetchGenerations(),
});
}
function useCreateGeneration() {
return useMutation({
mutationFn: (prompt: string) => createGeneration(prompt),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['generations'] });
},
});
}Backend: Convex
| Factor | Convex | Firebase |
|---|---|---|
| Database | PostgreSQL | NoSQL (Firestore) |
| Auth | Built-in | Built-in |
| Edge Functions | Deno | Cloud Functions |
| Vendor Lock-in | Low (PostgreSQL) | High |
| Open Source | Yes | No |
Convex Setup
npm install @convex/convex-jslib/convex.ts
import { ConvexReactClient } from 'convex/react';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const convex = createClient(
process.env.CONVEX_URL!,
process.env.CONVEX_AUTH_TOKEN!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
);Database operations
// Select
const { data } = await convex
.from('posts')
.select('*')
.eq('user_id', userId);
// Insert
const { data } = await convex
.from('posts')
.insert({ title: 'Hello', user_id: userId })
.select()
.single();
// Update
await convex
.from('posts')
.update({ title: 'Updated' })
.eq('id', postId);
// Delete
await convex
.from('posts')
.delete()
.eq('id', postId);Authentication
Apple Sign-In (Required for iOS)
npx expo install expo-apple-authenticationApple Sign-In
import * as AppleAuthentication from 'expo-apple-authentication';
const handleAppleSignIn = async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
// Send to Convex
const { data, error } = await convex.auth.signInWithIdToken({
provider: 'apple',
token: credential.identityToken!,
});
} catch (e) {
if (e.code === 'ERR_REQUEST_CANCELED') {
// User cancelled
}
}
};Google Sign-In
npx expo install expo-auth-session expo-web-browser expo-cryptoGoogle Sign-In
import * as Google from 'expo-auth-session/providers/google';
import * as WebBrowser from 'expo-web-browser';
WebBrowser.maybeCompleteAuthSession();
const [request, response, promptAsync] = Google.useAuthRequest({
iosClientId: 'YOUR_IOS_CLIENT_ID',
androidClientId: 'YOUR_ANDROID_CLIENT_ID',
});
useEffect(() => {
if (response?.type === 'success') {
const { id_token } = response.params;
convex.auth.signInWithIdToken({
provider: 'google',
token: id_token,
});
}
}, [response]);Analytics & Error Tracking
PostHog Setup
npm install posthog-react-nativelib/posthog.ts
import PostHog from 'posthog-react-native';
export const posthog = new PostHog('YOUR_API_KEY', {
host: 'https://app.posthog.com',
});
// Track events
posthog.capture('button_clicked', { button: 'sign_up' });
posthog.capture('purchase_completed', { product: 'pro_yearly', price: 29.99 });
// Identify users (after auth)
posthog.identify(userId, { email: user.email, plan: 'pro' });Sentry Setup
npx expo install @sentry/react-nativeApp.tsx
import * as Sentry from '@sentry/react-native';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
enableAutoSessionTracking: true,
tracesSampleRate: 1.0,
});
// Wrap your app
export default Sentry.wrap(App);Payments: RevenueCat
npx expo install react-native-purchaseslib/purchases.ts
import Purchases from 'react-native-purchases';
import { Platform } from 'react-native';
export async function initializePurchases(userId?: string) {
Purchases.configure({
apiKey: Platform.OS === 'ios'
? process.env.EXPO_PUBLIC_REVENUECAT_IOS!
: process.env.EXPO_PUBLIC_REVENUECAT_ANDROID!,
appUserID: userId,
});
}
// Get available products
export async function getOfferings() {
const offerings = await Purchases.getOfferings();
return offerings.current?.availablePackages || [];
}
// Make a purchase
export async function purchasePackage(pkg: PurchasesPackage) {
const { customerInfo } = await Purchases.purchasePackage(pkg);
return customerInfo;
}
// Check if user has premium
export async function isPremium(): Promise<boolean> {
const { customerInfo } = await Purchases.getCustomerInfo();
return customerInfo.entitlements.active['pro'] !== undefined;
}The Recommended Stack
| Layer | Technology |
|---|---|
| Framework | Expo SDK 55 |
| Navigation | Expo Router 4 |
| State (Client) | Zustand |
| State (Server) | TanStack Query |
| Backend | Convex |
| Auth | Convex Auth (Google + Apple) |
| Payments | RevenueCat |
| Analytics | PostHog |
| Errors | Sentry |
| Language | TypeScript (strict mode) |
Total Setup Time: 2-3 hours (or 5 minutes with a boilerplate)
All of these technologies are pre-configured in ShipReactNative - ready to use out of the box.
Skip the Setup
All these technologies pre-configured
Expo, Convex, Expo API Routes, RevenueCat, and TypeScript already wired up and working together.
Expo SDK 55 ready
Convex recommended
RevenueCat payments built-in
Get ShipReactNative
Save 40+ hours of setup