Mobile Development16 min read904 words

Expo and React Native in 2026: Complete Mobile Development Guide

Build production mobile apps with Expo and React Native. Learn project setup, navigation, native modules, EAS Build, and deployment best practices.

RP

Riken Patel

Expo has evolved into the recommended way to build React Native apps. With Expo SDK 52+, you get the best of both worlds: rapid development with managed workflow and full native access when needed. This guide covers building production apps with modern Expo.

Project Setup

bash
# Create new Expo project
npx create-expo-app@latest my-app --template tabs

cd my-app

# Install additional dependencies
npx expo install expo-router expo-status-bar
npx expo install @react-native-async-storage/async-storage
npx expo install expo-secure-store
npx expo install react-native-reanimated react-native-gesture-handler

# Start development
npx expo start
typescript
// app.json configuration
{
  "expo": {
    "name": "My App",
    "slug": "my-app",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "scheme": "myapp",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": ["**/*"],
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.company.myapp",
      "buildNumber": "1"
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.company.myapp",
      "versionCode": 1
    },
    "plugins": [
      "expo-router",
      "expo-secure-store",
      [
        "expo-camera",
        { "cameraPermission": "Allow $(PRODUCT_NAME) to access camera" }
      ]
    ],
    "experiments": {
      "typedRoutes": true
    }
  }
}
typescript
// File-based routing with Expo Router
// app/_layout.tsx
import { Stack } from 'expo-router';
import { ThemeProvider } from '@/providers/ThemeProvider';
import { AuthProvider } from '@/providers/AuthProvider';

export default function RootLayout() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <Stack
          screenOptions={{
            headerShown: false,
          }}
        >
          <Stack.Screen name="(tabs)" />
          <Stack.Screen 
            name="(auth)" 
            options={{ presentation: 'modal' }} 
          />
          <Stack.Screen 
            name="modal" 
            options={{ presentation: 'modal' }} 
          />
        </Stack>
      </ThemeProvider>
    </AuthProvider>
  );
}

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useTheme } from '@/hooks/useTheme';

export default function TabLayout() {
  const { colors } = useTheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: colors.primary,
        tabBarInactiveTintColor: colors.text.secondary,
        headerShown: true,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

// Type-safe navigation
import { router, useLocalSearchParams, Link } from 'expo-router';

// Navigate programmatically
router.push('/profile');
router.push({ pathname: '/product/[id]', params: { id: '123' } });
router.replace('/login');
router.back();

// Get route params
const { id } = useLocalSearchParams<{ id: string }>();

// Link component
<Link href="/settings" asChild>
  <Pressable>
    <Text>Settings</Text>
  </Pressable>
</Link>

State Management

typescript
// Zustand store with persistence
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface AuthState {
  user: User | null;
  token: string | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshToken: () => Promise<void>;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      token: null,
      isLoading: false,

      login: async (email, password) => {
        set({ isLoading: true });
        try {
          const response = await api.post('/auth/login', { email, password });
          const { user, token } = response.data;
          
          // Store token securely
          await SecureStore.setItemAsync('token', token);
          
          set({ user, token, isLoading: false });
        } catch (error) {
          set({ isLoading: false });
          throw error;
        }
      },

      logout: async () => {
        await SecureStore.deleteItemAsync('token');
        set({ user: null, token: null });
      },

      refreshToken: async () => {
        const currentToken = get().token;
        if (!currentToken) return;
        
        const response = await api.post('/auth/refresh', { token: currentToken });
        const { token } = response.data;
        
        await SecureStore.setItemAsync('token', token);
        set({ token });
      },
    }),
    {
      name: 'auth-storage',
      storage: createJSONStorage(() => AsyncStorage),
      partialize: (state) => ({ user: state.user }), // Only persist user
    }
  )
);

// React Query for server state
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

export function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: () => api.get('/products').then(r => r.data),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

export function useCreateOrder() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (order: CreateOrderDTO) => api.post('/orders', order),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

EAS Build and Submit

json
// eas.json
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": false
      },
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "autoIncrement": true,
      "ios": {
        "resourceClass": "m1-medium"
      },
      "android": {
        "buildType": "app-bundle"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@email.com",
        "ascAppId": "1234567890",
        "appleTeamId": "XXXXXXXXXX"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services.json",
        "track": "internal"
      }
    }
  }
}
bash
# Build commands

# Development build (with dev client)
eas build --profile development --platform ios
eas build --profile development --platform android

# Preview build for testing
eas build --profile preview --platform all

# Production build
eas build --profile production --platform all

# Submit to stores
eas submit --platform ios
eas submit --platform android

# OTA updates
eas update --branch production --message "Bug fixes"

Best Practices

Expo Best Practices 2026

Development:

- Use Expo Router for file-based navigation

- Enable typed routes for type safety

- Use development builds for native testing

Performance:

- Use React Native Reanimated for animations

- Implement proper list virtualization

- Use Expo Image for optimized images

Deployment:

- Use EAS Build for CI/CD

- Implement OTA updates for quick fixes

- Use preview builds for QA testing

Conclusion

Expo has become the best way to build React Native apps in 2026. The combination of managed workflow convenience with full native access, plus EAS for builds and updates, provides a complete mobile development platform.

Need help building your mobile app? Contact Jishu Labs for expert React Native and Expo development.

RP

About Riken Patel

Riken Patel is the Mobile Lead at Jishu Labs with extensive experience in React Native and cross-platform mobile development.

Related Articles

Mobile Development18 min read

React Native New Architecture: Complete Migration Guide for 2026

Master the React Native New Architecture with Fabric renderer and TurboModules. Learn migration strategies, performance optimizations, and best practices for building high-performance cross-platform mobile applications.

Emily Rodriguez

January 14, 2026

Mobile Development18 min read

SwiftUI for iOS Development 2026: Building Modern Apple Apps

Master SwiftUI for building modern iOS, macOS, watchOS, and visionOS applications. Learn declarative UI patterns, state management, navigation, and performance optimization techniques.

Riken Patel

December 28, 2025

Ready to Build Your Next Project?

Let's discuss how our expert team can help bring your vision to life.

Top-Rated
Software Development
Company

Ready to Get Started?

Get consistent results. Collaborate in real-time.
Build Intelligent Apps. Work with Jishu Labs.

SCHEDULE MY CALL