ScriptLabs

ServicesTechnologyPortfolioAboutInsightsContact
Sign InSign Up
HomeInsightsBlogMobile Development
Mobile Development

Building Cross-Platform Mobile Apps with React Native and Expo SDK 50

Complete guide to modern mobile development

ScriptLabs Team
ScriptLabs Team
Engineering
January 8, 2024
5 min read

The Mobile Development Revolution

Expo SDK 50 represents a major milestone in React Native development, bringing unprecedented developer experience improvements and production-ready features for building cross-platform mobile applications that rival native performance.

At ScriptLabs Studios, we've been leveraging Expo's ecosystem to deliver high-quality mobile applications faster and more efficiently than ever before.

What's New in Expo SDK 50

File-Based Routing with Expo Router

File-based routing makes navigation intuitive and maintainable with the new Expo Router v3:

// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    
      
      
      
    
  );
}

// app/index.tsx
import { Link } from 'expo-router';
import { View, Text } from 'react-native';

export default function HomePage() {
  return (
    
      Welcome to our app!
      Go to Profile
    
  );
}

Enhanced Local-First Architecture

Built-in support for offline-first applications:

import { SQLiteProvider, useSQLiteContext } from 'expo-sqlite/next';
import NetInfo from '@react-native-async-storage/async-storage';

function DatabaseApp() {
  return (
    
      
    
  );
}

function useOfflineSync() {
  const db = useSQLiteContext();
  const [isOnline, setIsOnline] = useState(false);
  
  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener(state => {
      setIsOnline(state.isConnected);
      if (state.isConnected) {
        syncWithServer();
      }
    });
    
    return unsubscribe;
  }, []);
  
  const syncWithServer = async () => {
    const pendingChanges = await db.getAllAsync(
      'SELECT * FROM sync_queue WHERE synced = 0'
    );
    
    for (const change of pendingChanges) {
      await uploadChange(change);
      await db.runAsync(
        'UPDATE sync_queue SET synced = 1 WHERE id = ?',
        [change.id]
      );
    }
  };
}

Performance Optimizations

New Architecture (Fabric & TurboModules)

Expo SDK 50 fully embraces React Native's New Architecture:

  • Fabric Renderer - Improved UI rendering performance
  • TurboModules - Faster native module loading
  • Concurrent Features - Better handling of updates and rendering
  • JSI Integration - Direct JavaScript-to-native communication
// expo-module.config.json
{
  "platforms": ["ios", "android"],
  "ios": {
    "fabric": true,
    "turboModules": true
  },
  "android": {
    "fabric": true,
    "turboModules": true
  }
}

Optimized Bundle Splitting

Automatic code splitting and lazy loading:

// Using React.lazy with Expo Router
import { lazy } from 'react';

const ProfileScreen = lazy(() => import('./ProfileScreen'));
const SettingsScreen = lazy(() => import('./SettingsScreen'));

// app/(tabs)/_layout.tsx
export default function TabLayout() {
  return (
    
        }}
      />
        }}
      />
    
  );
}

Native Module Integration

Expo Modules API

Creating custom native modules is now incredibly straightforward:

// modules/custom-camera/src/CustomCameraModule.ts
import { NativeModule, requireNativeModule } from 'expo-modules-core';

const CustomCameraModule = requireNativeModule('CustomCamera');

export default {
  async takePictureAsync(options: CameraOptions): Promise {
    return await CustomCameraModule.takePictureAsync(options);
  },
  
  async startRecording(): Promise {
    return await CustomCameraModule.startRecording();
  }
};

// iOS implementation (Swift)
// modules/custom-camera/ios/CustomCameraModule.swift
import ExpoModulesCore
import AVFoundation

public class CustomCameraModule: Module {
  public func definition() -> ModuleDefinition {
    Name("CustomCamera")
    
    AsyncFunction("takePictureAsync") { (options: CameraOptions) -> CameraPicture in
      return try await capturePhoto(with: options)
    }
    
    AsyncFunction("startRecording") {
      try await startVideoRecording()
    }
  }
}

Device Integration

Seamless access to device capabilities:

import * as Camera from 'expo-camera';
import * as MediaLibrary from 'expo-media-library';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';

export function useDeviceCapabilities() {
  const [permissions, setPermissions] = useState({});
  
  useEffect(() => {
    requestPermissions();
  }, []);
  
  const requestPermissions = async () => {
    const [
      cameraStatus,
      mediaStatus,
      locationStatus,
      notificationStatus
    ] = await Promise.all([
      Camera.requestCameraPermissionsAsync(),
      MediaLibrary.requestPermissionsAsync(),
      Location.requestForegroundPermissionsAsync(),
      Notifications.requestPermissionsAsync()
    ]);
    
    setPermissions({
      camera: cameraStatus.granted,
      media: mediaStatus.granted,
      location: locationStatus.granted,
      notifications: notificationStatus.granted
    });
  };
  
  return permissions;
}

State Management & Data Flow

Zustand Integration

Lightweight state management that works perfectly with Expo:

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

interface UserStore {
  user: User | null;
  preferences: UserPreferences;
  setUser: (user: User) => void;
  updatePreferences: (prefs: Partial) => void;
  clearUser: () => void;
}

export const useUserStore = create()(
  persist(
    (set) => ({
      user: null,
      preferences: defaultPreferences,
      
      setUser: (user) => set({ user }),
      
      updatePreferences: (prefs) => 
        set((state) => ({ 
          preferences: { ...state.preferences, ...prefs } 
        })),
      
      clearUser: () => set({ user: null })
    }),
    {
      name: 'user-storage',
      storage: createJSONStorage(() => AsyncStorage)
    }
  )
);

Real-time Data Synchronization

Using Expo's WebSocket support for live updates:

import { useEffect, useState } from 'react';

export function useRealtimeData(endpoint: string) {
  const [data, setData] = useState(null);
  const [isConnected, setIsConnected] = useState(false);
  
  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/' + endpoint);
    
    ws.onopen = () => setIsConnected(true);
    ws.onclose = () => setIsConnected(false);
    
    ws.onmessage = (event) => {
      const update = JSON.parse(event.data);
      setData(prevData => applyUpdate(prevData, update));
    };
    
    return () => ws.close();
  }, [endpoint]);
  
  return { data, isConnected };
}

UI/UX Best Practices

Responsive Design

Creating adaptive layouts for different screen sizes:

import { useDeviceOrientation } from '@react-native-community/hooks';
import { Dimensions } from 'react-native';

export function useResponsiveLayout() {
  const orientation = useDeviceOrientation();
  const { width, height } = Dimensions.get('window');
  
  const isTablet = Math.min(width, height) >= 768;
  const isLandscape = orientation.landscape;
  
  return {
    isTablet,
    isLandscape,
    columns: isTablet ? (isLandscape ? 3 : 2) : 1,
    spacing: isTablet ? 16 : 8,
    fontSize: {
      small: isTablet ? 14 : 12,
      medium: isTablet ? 18 : 16,
      large: isTablet ? 24 : 20
    }
  };
}

Accessibility Implementation

import { View, Text, TouchableOpacity } from 'react-native';

function AccessibleButton({ onPress, children, disabled }) {
  return (
    
      
        
          {children}
        
      
    
  );
}

Testing Strategy

End-to-End Testing with Detox

// e2e/login.test.js
import { by, device, expect, element } from 'detox';

describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should login successfully with valid credentials', async () => {
    await element(by.id('email-input'))
      .typeText('user@example.com');
    
    await element(by.id('password-input'))
      .typeText('password123');
    
    await element(by.id('login-button')).tap();
    
    await expect(element(by.id('dashboard')))
      .toBeVisible();
  });
});

Deployment & Distribution

EAS Build & Submit

Streamlined build and deployment process:

# eas.json
{
  "cli": {
    "version": ">= 5.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "channel": "production"
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your-apple-id@example.com",
        "ascAppId": "1234567890"
      },
      "android": {
        "serviceAccountKeyPath": "./service-account.json",
        "track": "production"
      }
    }
  }
}

Over-the-Air Updates

import * as Updates from 'expo-updates';

export function useOTAUpdates() {
  const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
  const [isUpdating, setIsUpdating] = useState(false);
  
  useEffect(() => {
    checkForUpdates();
  }, []);
  
  const checkForUpdates = async () => {
    try {
      const update = await Updates.checkForUpdateAsync();
      setIsUpdateAvailable(update.isAvailable);
    } catch (error) {
      console.error('Update check failed:', error);
    }
  };
  
  const downloadAndInstallUpdate = async () => {
    setIsUpdating(true);
    try {
      await Updates.fetchUpdateAsync();
      await Updates.reloadAsync();
    } catch (error) {
      console.error('Update failed:', error);
      setIsUpdating(false);
    }
  };
  
  return {
    isUpdateAvailable,
    isUpdating,
    updateApp: downloadAndInstallUpdate
  };
}

Performance Monitoring

Analytics Integration

import * as Analytics from 'expo-analytics-amplitude';
import * as Sentry from 'sentry-expo';

// Initialize analytics
Analytics.initialize('your-amplitude-api-key');
Sentry.init({ dsn: 'your-sentry-dsn' });

export const analytics = {
  track: (eventName: string, properties?: object) => {
    Analytics.track(eventName, properties);
  },
  
  setUserId: (userId: string) => {
    Analytics.setUserId(userId);
  },
  
  logError: (error: Error, context?: object) => {
    Sentry.captureException(error, { extra: context });
  }
};

// Usage in components
function ProductScreen({ productId }) {
  useEffect(() => {
    analytics.track('Product View', { productId });
  }, [productId]);
  
  return ;
}

Conclusion

Expo SDK 50 has transformed mobile development, offering a comprehensive platform that combines the flexibility of React Native with the reliability of native performance. With file-based routing, enhanced offline capabilities, seamless native module integration, and powerful deployment tools, building production-ready mobile applications has never been more accessible.

The combination of developer experience improvements and production-ready features makes Expo SDK 50 an excellent choice for businesses looking to build high-quality mobile applications efficiently.

Ready to build your next mobile app with Expo? Let's discuss how these modern tools can accelerate your mobile development timeline.