React Native in 2026: Expo-First Era
React Native in 2026 means Expo. The React Native community's shift to Expo is complete — the New Architecture (JSI, Fabric, TurboModules) ships stable, and Expo SDK 52 supports it fully. For new React Native projects, starting without Expo is now the exception, not the rule.
For JavaScript developers building mobile SaaS, React Native's JavaScript/TypeScript foundation and shared ecosystem with web development makes it the most natural choice.
Quick Comparison
| Starter | Price | Auth | Billing | Navigation | Testing | Best For |
|---|---|---|---|---|---|---|
| Expo Router Starter | Free | ❌ | ❌ | File-based | ❌ | Modern Expo baseline |
| Ignite | Free | JWT | ❌ | React Navigation | MobX-State-Tree | Teams, production apps |
| Expo + Supabase | Free | Supabase Auth | ❌ | Expo Router | ❌ | Supabase-backed apps |
| t4-app | Free | Clerk/Supabase | ❌ | Expo Router | ❌ | Next.js + Expo monorepo |
| ShipFlutter | $149 | Firebase/Supabase | RevenueCat | — | ❌ | ⚠️ Flutter, not RN |
The Starters
Expo Router Starter — Best Baseline
Price: Free | Creator: Expo team
The official Expo starter for Expo Router v3. File-based routing (like Next.js but for mobile), tab navigation, stack navigation, TypeScript, and shared code patterns. The right foundation for any modern React Native app.
npx create-expo-app@latest --template
# Choose: Navigation (Tabs) template
Choose if: You want the current official Expo baseline with file-based routing.
Ignite — Best Production Architecture
Price: Free | Creator: Infinite Red
The battle-tested React Native boilerplate. MobX-State-Tree state management, React Navigation, TypeScript, Reactotron debugging, AsyncStorage, i18n, and Jest testing. Actively maintained since 2016 with a large community.
npx ignite-cli@latest new MyApp
# Generates complete project structure with:
# models/ (MST stores)
# screens/ (with pre-built examples)
# components/ (design system)
# services/ (API layer)
# i18n/ (multi-language)
Choose if: You're building a serious production app with a team and need a proven architecture.
Expo + Supabase Template — Best Free Backend
Price: Free | Creator: Supabase team
Official Expo + Supabase integration template. Supabase Auth (email/password, OAuth), Supabase client setup, protected routes, and TypeScript. The fastest way to get a React Native app talking to a PostgreSQL backend.
Choose if: You're using Supabase and want an official starting point.
t4-app — Best Monorepo
Price: Free | Creator: Tim Miller
Type-safe, universal monorepo for Next.js (web) and Expo (mobile) sharing code. Tamagui UI components, tRPC, Solito navigation abstraction, and Supabase/Clerk auth. Write UI components once, use on web and mobile.
packages/
├── ui/ # Tamagui components (web + mobile)
├── api/ # tRPC router
└── db/ # Prisma schema + migrations
apps/
├── next/ # Next.js web app
└── expo/ # Expo mobile app
Choose if: You're building a SaaS that needs both a web app and mobile app sharing UI code.
Mobile Billing for React Native
In-app purchases are mandatory for App Store distribution and recommended for Play Store:
RevenueCat
import Purchases from 'react-native-purchases';
// Initialize
await Purchases.configure({ apiKey: 'YOUR_KEY' });
// Fetch offerings
const offerings = await Purchases.getOfferings();
const monthly = offerings.current?.availablePackages.find(
p => p.packageType === PACKAGE_TYPE.MONTHLY
);
// Purchase
const { customerInfo } = await Purchases.purchasePackage(monthly);
RevenueCat handles the platform differences between StoreKit (iOS) and Google Billing (Android) behind a unified API.
Stripe (Web Billing via WebView)
For apps that charge via web rather than in-app purchases:
import { WebView } from 'react-native-webview';
// Open Stripe Checkout in WebView
<WebView
source={{ uri: `https://yourapp.com/checkout?userId=${userId}` }}
onNavigationStateChange={handleNavigationChange}
/>
React Native vs Flutter Decision
| Factor | React Native | Flutter |
|---|---|---|
| Language | JavaScript/TypeScript | Dart |
| Team background | Web developers | Any background |
| Web code sharing | ✅ (t4-app pattern) | ⚠️ (separate web strategy) |
| Native look | ✅ Uses native components | ⚠️ Custom rendering |
| Performance | Good (New Architecture) | Excellent (Impeller) |
| OTA updates | ✅ Expo EAS Update | ❌ Not possible |
| Ecosystem | npm (massive) | pub.dev (growing) |
Rule of thumb: If your team builds web apps with React, use React Native. If you're starting fresh and want the best cross-platform performance, evaluate Flutter.
Authentication in React Native: Token Storage
Mobile auth requires handling tokens differently from web apps. The key difference: localStorage doesn't exist. Use Expo SecureStore for token persistence — it maps to the iOS Keychain and Android EncryptedSharedPreferences.
// utils/auth-storage.ts
import * as SecureStore from 'expo-secure-store';
export const authStorage = {
async setToken(token: string) {
await SecureStore.setItemAsync('auth_token', token);
},
async getToken(): Promise<string | null> {
return SecureStore.getItemAsync('auth_token');
},
async clearToken() {
await SecureStore.deleteItemAsync('auth_token');
},
};
For OAuth flows (Google, GitHub), Expo AuthSession handles the redirect URI pattern that works across iOS, Android, and Expo Go:
import * as AuthSession from 'expo-auth-session';
import * as Google from 'expo-auth-session/providers/google';
const [request, response, promptAsync] = Google.useAuthRequest({
clientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID,
redirectUri: AuthSession.makeRedirectUri({ scheme: 'yourapp' }),
});
// Trigger OAuth
<Button onPress={() => promptAsync()} title="Sign in with Google" />
Never store tokens in AsyncStorage — it's unencrypted and accessible without authentication on rooted/jailbroken devices.
OTA Updates with Expo EAS
Expo's EAS Update lets you push JavaScript changes to production without App Store review cycles:
# Install EAS CLI
npm install -g eas-cli
eas login
# Configure EAS Update
eas update:configure
# Push update to production channel
eas update --branch production --message "Fix checkout error"
OTA updates work for JavaScript/TypeScript and asset changes. Adding or modifying native modules (new Expo plugins, native dependencies) requires a full rebuild. In practice, most bug fixes and feature additions are JavaScript-only — OTA covers 80-90% of production updates.
This is React Native's most significant advantage over Flutter for SaaS products: you can ship hotfixes to production in minutes rather than waiting 24-48 hours for App Store review.
Deploying with EAS Build
EAS Build handles iOS and Android compilation in the cloud, without requiring macOS or Android Studio locally:
// eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
}
}
# Build for both platforms
eas build --platform all --profile production
# Submit to App Store and Play Store
eas submit --platform all
EAS Build starts at free for open source projects and $29/month for commercial apps. It replaces the complex Xcode/Android Studio CI setup that previously required dedicated macOS build machines.
State Management for React Native SaaS
Mobile apps have two distinct types of state: server state (data from your backend) and client state (UI state, preferences, temporary data). The right tools are different for each.
TanStack Query for server state:
import { useQuery, useMutation } from '@tanstack/react-query';
// Fetch and cache subscription status
export function useSubscription() {
return useQuery({
queryKey: ['subscription'],
queryFn: () => api.get('/api/subscription'),
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
// Update with optimistic UI
export function useUpdateProfile() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data) => api.patch('/api/profile', data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['profile'] }),
});
}
Zustand for client state:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
const useAppStore = create(
persist(
(set) => ({
onboardingCompleted: false,
selectedTheme: 'light' as 'light' | 'dark',
completeOnboarding: () => set({ onboardingCompleted: true }),
}),
{ name: 'app-storage', storage: createJSONStorage(() => AsyncStorage) }
)
);
This combination — TanStack Query for network data, Zustand for app state — is the React Native equivalent of the web pattern most developers already know.
Choosing the Right Architecture for Mobile SaaS
The choice between these starters maps to your product's complexity:
Expo Router Starter is the right foundation when you're early-stage and want to build on the official Expo patterns. Add auth, billing, and state management as your product grows — don't prematurely add complexity.
Ignite is the right foundation when you have a team or expect significant complexity. The MobX-State-Tree architecture is production-tested for large apps. The trade-off is initial complexity that pays off at scale.
t4-app is the right foundation when web + mobile code sharing is a product requirement, not just a preference. Running two separate codebases (Next.js and Expo) is often simpler than maintaining a monorepo unless UI reuse provides concrete value.
Expo + Supabase is the right foundation when you're already committed to Supabase for your backend. The combination of Supabase's real-time subscriptions, Row Level Security, and Storage with Expo's mobile capabilities is particularly strong for apps with collaborative or real-time features.
Testing React Native Apps
Testing is the area where React Native boilerplates vary most. Ignite ships with a working Jest + MMST test setup. Expo Router Starter ships with nothing. Here's the baseline test setup for any Expo app:
# Jest for unit tests
npm install --save-dev jest-expo @testing-library/react-native
# Detox for E2E on real devices/simulators
npm install --save-dev detox
// __tests__/auth.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { SignInScreen } from '../screens/SignIn';
test('shows error on invalid email', async () => {
const { getByPlaceholderText, getByText } = render(<SignInScreen />);
fireEvent.changeText(getByPlaceholderText('Email'), 'invalid');
fireEvent.press(getByText('Sign In'));
expect(getByText('Enter a valid email address')).toBeTruthy();
});
Unit tests with React Native Testing Library work the same as React Testing Library for web. E2E testing with Detox or Maestro runs tests on actual iOS simulators and Android emulators — slower to set up, but tests the actual native behavior.
What None of These Include
All four boilerplates above are foundations, not complete mobile SaaS products. Before launch, you'll need to add:
- Push notifications (Expo Notifications + backend integration)
- Analytics (Amplitude, Mixpanel, or Firebase Analytics)
- Error monitoring (Sentry for React Native)
- Deep links (for email verification, password reset flows)
- Offline support (if your SaaS needs to work without connectivity)
Budget 1-2 weeks for these additions on top of the core boilerplate setup. Planning these upfront — rather than adding them as afterthoughts — saves significant refactoring later, particularly for deep linking (which affects auth flows, notification handling, and marketing attribution) and analytics (which needs to be instrumented throughout the app from the start to be meaningful).
Navigation Patterns in React Native Boilerplates
Navigation is one of the most consequential architectural decisions in React Native because it affects every screen in the app. The React Native ecosystem has converged on two navigation libraries: Expo Router (file-based, the current recommended approach for new Expo projects) and React Navigation (imperative, the long-standing ecosystem standard). Understanding the difference determines which boilerplates to evaluate.
Expo Router uses a file-based routing convention similar to Next.js: files in the app/ directory map to routes, nested directories create nested navigators, and special files like _layout.tsx define stack and tab navigator configurations. Deep linking and universal links work automatically because the URL structure matches the file structure. Server-side rendering works for web targets. For developers coming from Next.js, Expo Router is immediately familiar. The trade-off is that it is a higher-level abstraction — when you need to customize navigation behavior in ways that Expo Router does not support, you are working against the abstraction rather than with it.
React Navigation is the lower-level, more configurable option. Stack, tab, drawer, and bottom sheet navigators are configured imperatively with TypeScript types. Deep linking requires explicit configuration in a linking object. The flexibility means React Navigation supports navigation patterns that Expo Router cannot — custom animated transitions, shared element transitions, modal stacks with complex dismiss behavior. The Ignite boilerplate uses React Navigation as its foundation because Infinite Red, the consultancy behind Ignite, regularly builds apps with complex custom navigation requirements.
For a standard SaaS app — authentication flow, main tab navigator, settings, subscription management — Expo Router handles everything cleanly. If your product has custom navigation animations or unusual navigation patterns as a core part of its UX, React Navigation's flexibility is worth the additional configuration. Most new Expo boilerplates launched in 2025-2026 use Expo Router; the shift from React Navigation to Expo Router as the default is one of the more significant ecosystem changes of the past two years.
Evaluating Expo SDK Version Compatibility
One of the most common sources of friction with React Native boilerplates is Expo SDK version mismatch. Expo releases a new SDK approximately every 3-4 months, and each SDK version pins specific versions of React Native, React, and all bundled native modules. A boilerplate built on Expo SDK 51 uses React Native 0.74 and React 18.2. If you install a library that requires React 19 or React Native 0.75, you'll get a compatibility error.
Before installing a React Native boilerplate, check the Expo SDK version in package.json and compare it against the current stable SDK on the Expo changelog. A boilerplate running an SDK version that is two or more releases behind the current stable is likely to have accumulated breaking changes in native dependencies. The Expo SDK upgrade guide covers each version's breaking changes, and the tooling (npx expo-doctor) diagnoses compatibility issues automatically.
The deeper issue with SDK versions is the expo-modules-core dependency graph. Third-party libraries that use native modules (camera, biometrics, file system) must be built against the same Expo SDK version as your app. The Expo ecosystem has largely standardized on the new modules API, but older libraries that haven't been updated will produce native build failures that are difficult to diagnose without knowing this context. The Expo SDK compatibility table documents which library versions work with which SDK — bookmarking it before starting a new project saves hours of debugging.
For teams who want web + mobile from a single codebase, the best Expo + Next.js shared boilerplates guide covers the monorepo patterns in detail. For understanding how React Native compares to Flutter's equivalent ecosystem constraints, see the best Flutter boilerplates guide.
Billing in React Native: In-App Purchases vs Web Subscriptions
Billing is the area where React Native mobile SaaS diverges most sharply from web SaaS. Stripe, the default choice for web, cannot process in-app purchases — Apple and Google require all digital goods sold within iOS and Android apps to use their native payment systems (App Store IAP and Google Play Billing respectively). Understanding this before starting saves a significant architecture surprise.
The practical choices for a React Native SaaS billing setup:
RevenueCat is the de facto standard for React Native in-app purchases. It abstracts Apple IAP and Google Play Billing behind a single API, syncs entitlements to your backend, and handles receipt validation. The SDK integrates with Expo and bare React Native. Free up to $2,500 MRR, then 1% of revenue. For most early-stage mobile SaaS, RevenueCat eliminates months of IAP complexity.
Stripe (web only): If your app has a web component — even a simple "manage subscription" page — users can subscribe on the web via Stripe and unlock features in the mobile app via entitlement sync. This is the legal path to avoid Apple's 30% commission on subscriptions. Apple allows linking to a web page for "account management," which courts have interpreted broadly enough to allow subscription sign-up. Many SaaS apps use this pattern: mobile app is free to download, subscription is handled through the web app, entitlements are synced via your backend.
The hybrid approach: Most mature React Native SaaS products use both — RevenueCat for users who subscribe natively on mobile (captures impulse purchases at the point of value), and Stripe on web for users who find the product through marketing channels. The revenue split varies by product but generally favors the channel with less friction at the point of first conversion.
None of the boilerplates covered in this article ship with RevenueCat pre-integrated. Budget 1-2 days to add it using the RevenueCat Expo SDK and their React Native quick-start documentation. Connecting RevenueCat entitlements to your existing database user model is the main integration task.
Compare all React Native starters in the StarterPick directory.
See our best Flutter boilerplates guide for the cross-platform alternative comparison.
Browse our best SaaS boilerplates guide for the complete mobile starter comparison.