DEV Community

Nilava Chowdhury
Nilava Chowdhury

Posted on

๐Ÿ“ฑ React Native at Scale: Lessons from 100K+ Users

Engineering a React Native app that handles millions of daily transactions

Introduction

When we launched udChalo's mobile app for armed forces personnel, we knew we were building for a unique audience with specific needs: reliability in low-connectivity areas, security for sensitive data, and performance that matches native apps. This post shares our journey from zero to 100K+ users in 6 months, achieving a 4.5-star rating while processing millions of flight bookings.

The Scale Challenge

Requirements and Constraints

const appRequirements = {
  scale: {
    targetUsers: 100000,
    dailyActiveUsers: 50000,
    peakConcurrent: 10000,
    transactionsPerDay: 100000,
    dataSync: '5GB daily'
  },

  performance: {
    coldStart: '<2 seconds',
    navigation: '<100ms',
    apiResponse: '<500ms',
    offlineCapability: 'Full feature parity',
    memoryUsage: '<150MB'
  },

  userEnvironment: {
    devices: '70% Android, 30% iOS',
    androidVersions: '60% < Android 8',
    networkQuality: '40% on 2G/3G',
    locations: 'Remote military bases',
    storage: 'Limited (2-4GB available)'
  },

  security: {
    encryption: 'Military-grade',
    authentication: 'Biometric + PIN',
    dataResidency: 'India only',
    compliance: 'Government standards'
  }
};
Enter fullscreen mode Exit fullscreen mode

Architecture Decisions

React Native Architecture

// App Architecture
const architecture = {
  core: {
    version: 'React Native 0.71',
    newArchitecture: true,  // Fabric + TurboModules
    hermes: true,  // For better performance
    reanimated: 3,  // For 60fps animations
  },

  stateManagement: {
    global: 'Redux Toolkit + RTK Query',
    local: 'React Hook Form',
    persistence: 'Redux Persist + MMKV',

  },

  navigation: {
    library: 'React Navigation 6',
    structure: 'Tab + Stack hybrid',
    deepLinking: true,

  },

  native: {
    modules: [
      'Biometric Authentication',
      'Secure Storage',
      'Network Detection',
      'Background Sync',
      'Push Notifications'
    ],

  }
};

// Folder Structure for Scale
const projectStructure = `
src/
โ”œโ”€โ”€ components/          # Shared components
โ”‚   โ”œโ”€โ”€ atoms/          # Basic building blocks
โ”‚   โ”œโ”€โ”€ molecules/      # Composite components
โ”‚   โ””โ”€โ”€ organisms/      # Complex components
โ”œโ”€โ”€ screens/            # Screen components
โ”œโ”€โ”€ navigation/         # Navigation configuration
โ”œโ”€โ”€ services/           # API and external services
โ”œโ”€โ”€ store/              # Redux store and slices
โ”œโ”€โ”€ utils/              # Utility functions
โ”œโ”€โ”€ hooks/              # Custom hooks
โ”œโ”€โ”€ native/             # Native module bridges
โ””โ”€โ”€ constants/          # App constants
`;
Enter fullscreen mode Exit fullscreen mode

Performance Optimization Strategy

// Performance-First Component Design
import React, { memo, useMemo, useCallback } from 'react';
import { FlatList, View, Text } from 'react-native';
import FastImage from 'react-native-fast-image';

interface FlightListProps {
  flights: Flight[];
  onSelect: (flight: Flight) => void;
}

// Optimized Flight List Component
export const FlightList = memo<FlightListProps>(({ flights, onSelect }) => {
  // Memoize expensive calculations
  const sortedFlights = useMemo(() => 
    flights.sort((a, b) => a.price - b.price),
    [flights]
  );

  // Optimize callbacks
  const renderItem = useCallback(({ item }: { item: Flight }) => (
    <FlightCard flight={item} onPress={() => onSelect(item)} />
  ), [onSelect]);

  const keyExtractor = useCallback((item: Flight) => item.id, []);

  const getItemLayout = useCallback((data: any, index: number) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  }), []);

  return (
    <FlatList
      data={sortedFlights}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}

      // Performance optimizations
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={50}
      initialNumToRender={10}
      windowSize={10}

      // Memory optimization
      maintainVisibleContentPosition={{
        minIndexForVisible: 0,
      }}
    />
  );
});

// Optimized Image Component
const FlightAirlineLogo = memo(({ airline }: { airline: string }) => {
  const imageSource = useMemo(() => ({
    uri: getAirlineLogoUrl(airline),
    priority: FastImage.priority.normal,
    cache: FastImage.cacheControl.immutable,
  }), [airline]);

  return (
    <FastImage
      style={styles.airlineLogo}
      source={imageSource}
      resizeMode={FastImage.resizeMode.contain}
    />
  );
});
Enter fullscreen mode Exit fullscreen mode

Native Module Implementation

Biometric Authentication Module

// Android Native Module - BiometricAuthModule.java
package com.udchalo.biometric;

import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.FragmentActivity;
import com.facebook.react.bridge.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class BiometricAuthModule extends ReactContextBaseJavaModule {
    private final ReactApplicationContext reactContext;
    private final Executor executor = Executors.newSingleThreadExecutor();

    public BiometricAuthModule(ReactApplicationContext reactContext) {
        super(reactContext);
        this.reactContext = reactContext;
    }

    @Override
    public String getName() {
        return "BiometricAuth";
    }

    @ReactMethod
    public void authenticate(String reason, Promise promise) {
        FragmentActivity activity = (FragmentActivity) getCurrentActivity();

        if (activity == null) {
            promise.reject("E_ACTIVITY_NULL", "Activity is null");
            return;
        }

        activity.runOnUiThread(() -> {
            BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("Authentication Required")
                .setSubtitle(reason)
                .setNegativeButtonText("Cancel")
                .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
                .build();

            BiometricPrompt biometricPrompt = new BiometricPrompt(
                activity,
                executor,
                new BiometricPrompt.AuthenticationCallback() {
                    @Override
                    public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                        promise.resolve(true);
                    }

                    @Override
                    public void onAuthenticationError(int errorCode, CharSequence errString) {
                        promise.reject("E_AUTH_FAILED", errString.toString());
                    }

                    @Override
                    public void onAuthenticationFailed() {
                        promise.reject("E_AUTH_FAILED", "Authentication failed");
                    }
                }
            );

            biometricPrompt.authenticate(promptInfo);
        });
    }

    @ReactMethod
    public void isAvailable(Promise promise) {
        BiometricManager biometricManager = BiometricManager.from(reactContext);

        switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            case BiometricManager.BIOMETRIC_SUCCESS:
                promise.resolve(true);
                break;
            default:
                promise.resolve(false);
                break;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
// iOS Native Module - BiometricAuth.swift
import LocalAuthentication
import React

@objc(BiometricAuth)
class BiometricAuth: NSObject {

    @objc(authenticate:resolver:rejecter:)
    func authenticate(reason: String,
                     resolver: @escaping RCTPromiseResolveBlock,
                     rejecter: @escaping RCTPromiseRejectBlock) {

        let context = LAContext()
        var error: NSError?

        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
            rejecter("E_BIOMETRIC_NOT_AVAILABLE", "Biometric authentication not available", error)
            return
        }

        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
                             localizedReason: reason) { success, error in
            DispatchQueue.main.async {
                if success {
                    resolver(true)
                } else {
                    rejecter("E_AUTH_FAILED", "Authentication failed", error)
                }
            }
        }
    }

    @objc(isAvailable:rejecter:)
    func isAvailable(resolver: RCTPromiseResolveBlock,
                    rejecter: RCTPromiseRejectBlock) {
        let context = LAContext()
        var error: NSError?

        let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
        resolver(available)
    }
}
Enter fullscreen mode Exit fullscreen mode

Background Sync Implementation

// Background Sync Manager
import BackgroundFetch from 'react-native-background-fetch';
import NetInfo from '@react-native-community/netinfo';
import { MMKV } from 'react-native-mmkv';

class BackgroundSyncManager {
  private storage = new MMKV();
  private syncQueue: SyncTask[] = [];

  async initialize() {
    await BackgroundFetch.configure({
      minimumFetchInterval: 15,  // 15 minutes
      forceAlarmManager: false,
      stopOnTerminate: false,
      startOnBoot: true,
      enableHeadless: true,
      requiresBatteryNotLow: false,
      requiresCharging: false,
      requiresStorageNotLow: false,
      requiresDeviceIdle: false,
      requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
    }, async (taskId) => {
      await this.performSync();
      BackgroundFetch.finish(taskId);
    }, (error) => {
      console.error('Background fetch failed:', error);
    });

    // Check for pending sync on app launch
    await this.checkPendingSync();
  }

  async queueForSync(data: any, type: SyncType) {
    const task: SyncTask = {
      id: generateUUID(),
      type,
      data,
      timestamp: Date.now(),
      attempts: 0,
      status: 'pending'
    };

    // Store in persistent queue
    const queue = this.getSyncQueue();
    queue.push(task);
    this.storage.set('syncQueue', JSON.stringify(queue));

    // Try immediate sync if online
    const netInfo = await NetInfo.fetch();
    if (netInfo.isConnected) {
      await this.performSync();
    }
  }

  async performSync() {
    const queue = this.getSyncQueue();
    const netInfo = await NetInfo.fetch();

    if (!netInfo.isConnected || queue.length === 0) {
      return;
    }

    const pendingTasks = queue.filter(task => task.status === 'pending');

    for (const task of pendingTasks) {
      try {
        await this.syncTask(task);
        task.status = 'completed';
      } catch (error) {
        task.attempts++;
        task.lastError = error.message;

        if (task.attempts >= 3) {
          task.status = 'failed';
        }
      }
    }

    // Update queue
    this.storage.set('syncQueue', JSON.stringify(queue));

    // Clean old completed tasks
    this.cleanupQueue();
  }

  private async syncTask(task: SyncTask) {
    switch (task.type) {
      case 'booking':
        return await this.syncBooking(task.data);
      case 'profile':
        return await this.syncProfile(task.data);
      case 'preferences':
        return await this.syncPreferences(task.data);
      default:
        throw new Error(`Unknown sync type: ${task.type}`);
    }
  }

  private getSyncQueue(): SyncTask[] {
    const queueString = this.storage.getString('syncQueue');
    return queueString ? JSON.parse(queueString) : [];
  }
}
Enter fullscreen mode Exit fullscreen mode

State Management at Scale

Redux Toolkit Setup

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { persistStore, persistReducer } from 'redux-persist';
import { MMKV } from 'react-native-mmkv';

// Custom MMKV storage adapter for Redux Persist
const storage = new MMKV();
const MMKVStorage = {
  setItem: (key: string, value: string) => {
    storage.set(key, value);
    return Promise.resolve(true);
  },
  getItem: (key: string) => {
    const value = storage.getString(key);
    return Promise.resolve(value);
  },
  removeItem: (key: string) => {
    storage.delete(key);
    return Promise.resolve();
  },
};

// RTK Query API
const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: Config.API_BASE_URL,
    prepareHeaders: (headers, { getState }) => {
      const token = (getState() as RootState).auth.token;
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['Flight', 'Booking', 'User'],
  endpoints: (builder) => ({
    searchFlights: builder.query<Flight[], FlightSearchParams>({
      query: (params) => ({
        url: '/flights/search',
        params,
      }),
      providesTags: ['Flight'],
      // Cache for 5 minutes
      keepUnusedDataFor: 300,
    }),
    createBooking: builder.mutation<Booking, CreateBookingDto>({
      query: (booking) => ({
        url: '/bookings',
        method: 'POST',
        body: booking,
      }),
      invalidatesTags: ['Booking'],
      // Optimistic update
      async onQueryStarted(booking, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          apiSlice.util.updateQueryData('getUserBookings', undefined, (draft) => {
            draft.unshift(booking as any);
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo();
        }
      },
    }),
  }),
});

// Configure store with persistence
const persistConfig = {
  key: 'root',
  storage: MMKVStorage,
  whitelist: ['auth', 'user', 'preferences'],
  blacklist: ['api'], // Don't persist API cache
};

const rootReducer = combineReducers({
  [apiSlice.reducerPath]: apiSlice.reducer,
  auth: authSlice.reducer,
  user: userSlice.reducer,
  preferences: preferencesSlice.reducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }).concat(apiSlice.middleware),
});

setupListeners(store.dispatch);
export const persistor = persistStore(store);
Enter fullscreen mode Exit fullscreen mode

Performance Monitoring

Custom Performance Metrics

// Performance Monitor
import performance from 'react-native-performance';
import analytics from '@react-native-firebase/analytics';

class PerformanceMonitor {
  private marks = new Map<string, number>();
  private measures = new Map<string, number[]>();

  startMark(name: string) {
    this.marks.set(name, performance.now());
  }

  endMark(name: string, customProperties?: Record<string, any>) {
    const startTime = this.marks.get(name);
    if (!startTime) return;

    const duration = performance.now() - startTime;

    // Store measure
    if (!this.measures.has(name)) {
      this.measures.set(name, []);
    }
    this.measures.get(name)!.push(duration);

    // Send to analytics if significant
    if (duration > this.getThreshold(name)) {
      analytics().logEvent('performance_metric', {
        metric_name: name,
        duration,
        ...customProperties,
      });
    }

    // Clean up
    this.marks.delete(name);
  }

  private getThreshold(name: string): number {
    const thresholds: Record<string, number> = {
      app_launch: 2000,
      screen_transition: 500,
      api_call: 1000,
      image_load: 500,
      list_render: 100,
    };

    return thresholds[name] || 1000;
  }

  reportMetrics() {
    const report: Record<string, any> = {};

    this.measures.forEach((durations, name) => {
      report[name] = {
        avg: durations.reduce((a, b) => a + b, 0) / durations.length,
        min: Math.min(...durations),
        max: Math.max(...durations),
        p95: this.calculatePercentile(durations, 0.95),
        count: durations.length,
      };
    });

    // Send to monitoring service
    analytics().logEvent('performance_report', report);

    // Clear old data
    this.measures.clear();
  }

  private calculatePercentile(values: number[], percentile: number): number {
    const sorted = values.sort((a, b) => a - b);
    const index = Math.ceil(sorted.length * percentile) - 1;
    return sorted[index];
  }
}
Enter fullscreen mode Exit fullscreen mode

Offline-First Architecture

Offline Data Sync

// Offline Manager
import { MMKV } from 'react-native-mmkv';
import NetInfo from '@react-native-community/netinfo';

class OfflineManager {
  private storage = new MMKV({ id: 'offline-data' });
  private syncInProgress = false;

  async saveOfflineData(key: string, data: any) {
    const offlineData = {
      data,
      timestamp: Date.now(),
      synced: false,
    };

    this.storage.set(key, JSON.stringify(offlineData));

    // Queue for sync
    await this.queueForSync(key);
  }

  async getOfflineData(key: string): Promise<any> {
    const stored = this.storage.getString(key);
    if (!stored) return null;

    const { data, timestamp, synced } = JSON.parse(stored);

    // Check if data is stale
    const isStale = Date.now() - timestamp > 24 * 60 * 60 * 1000; // 24 hours

    if (isStale && !synced) {
      // Try to sync
      await this.syncSingleItem(key);
    }

    return data;
  }

  async syncOfflineData() {
    if (this.syncInProgress) return;

    const netInfo = await NetInfo.fetch();
    if (!netInfo.isConnected) return;

    this.syncInProgress = true;

    try {
      const allKeys = this.storage.getAllKeys();
      const unsyncedKeys = allKeys.filter(key => {
        const data = this.storage.getString(key);
        if (!data) return false;
        const parsed = JSON.parse(data);
        return !parsed.synced;
      });

      for (const key of unsyncedKeys) {
        await this.syncSingleItem(key);
      }
    } finally {
      this.syncInProgress = false;
    }
  }

  private async syncSingleItem(key: string) {
    const stored = this.storage.getString(key);
    if (!stored) return;

    const { data, timestamp } = JSON.parse(stored);

    try {
      // Determine sync endpoint based on key pattern
      const endpoint = this.getEndpointForKey(key);

      const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          data,
          clientTimestamp: timestamp,
        }),
      });

      if (response.ok) {
        // Mark as synced
        const updated = {
          data,
          timestamp,
          synced: true,
          syncedAt: Date.now(),
        };
        this.storage.set(key, JSON.stringify(updated));
      }
    } catch (error) {
      console.error(`Failed to sync ${key}:`, error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Crash Reporting and Analytics

Crash Handler Setup

// Crash Reporter
import crashlytics from '@react-native-firebase/crashlytics';
import { ErrorBoundary } from 'react-error-boundary';

// Global error handler
ErrorUtils.setGlobalHandler((error: Error, isFatal: boolean) => {
  // Log to Crashlytics
  crashlytics().recordError(error, {
    isFatal,
    timestamp: Date.now(),
    userId: getCurrentUserId(),
    sessionId: getSessionId(),
  });

  // Log locally for debugging
  if (__DEV__) {
    console.error('Global error:', error);
  }

  // Show user-friendly error screen for fatal errors
  if (isFatal) {
    showErrorScreen(error);
  }
});

// React Error Boundary
function AppErrorBoundary({ children }: { children: React.ReactNode }) {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, errorInfo) => {
        crashlytics().log('React error boundary triggered');
        crashlytics().recordError(error, {
          errorBoundary: true,
          componentStack: errorInfo.componentStack,
        });
      }}
      onReset={() => {
        // Clear cache and restart app
        clearAppCache();
        RNRestart.Restart();
      }}
    >
      {children}
    </ErrorBoundary>
  );
}

// Custom crash reporter for specific scenarios
class CrashReporter {
  static logError(error: Error, context?: Record<string, any>) {
    const enrichedContext = {
      ...context,
      timestamp: Date.now(),
      appVersion: DeviceInfo.getVersion(),
      buildNumber: DeviceInfo.getBuildNumber(),
      device: {
        brand: DeviceInfo.getBrand(),
        model: DeviceInfo.getModel(),
        os: Platform.OS,
        osVersion: DeviceInfo.getSystemVersion(),
      },
      memory: {
        used: DeviceInfo.getUsedMemorySync(),
        total: DeviceInfo.getTotalMemorySync(),
      },
      network: getNetworkState(),
    };

    crashlytics().recordError(error, enrichedContext);

    // Send to custom monitoring
    if (shouldSendToCustomMonitoring(error)) {
      sendToCustomMonitoring(error, enrichedContext);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing Strategy

E2E Testing with Detox

// e2e/bookingFlow.test.js
describe('Booking Flow', () => {
  beforeAll(async () => {
    await device.launchApp({
      newInstance: true,
      permissions: { notifications: 'YES', location: 'always' },
    });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should complete flight booking successfully', async () => {
    // Login
    await element(by.id('email-input')).typeText('test@udchalo.com');
    await element(by.id('password-input')).typeText('Test@1234');
    await element(by.id('login-button')).tap();

    // Search flights
    await waitFor(element(by.id('search-screen')))
      .toBeVisible()
      .withTimeout(5000);

    await element(by.id('from-airport')).tap();
    await element(by.text('Delhi (DEL)')).tap();

    await element(by.id('to-airport')).tap();
    await element(by.text('Mumbai (BOM)')).tap();

    await element(by.id('departure-date')).tap();
    await element(by.text('15')).tap();

    await element(by.id('search-button')).tap();

    // Select flight
    await waitFor(element(by.id('flight-list')))
      .toBeVisible()
      .withTimeout(10000);

    await element(by.id('flight-item-0')).tap();

    // Passenger details
    await element(by.id('passenger-name')).typeText('John Doe');
    await element(by.id('passenger-phone')).typeText('9876543210');

    await element(by.id('continue-button')).tap();

    // Payment
    await element(by.id('payment-method-upi')).tap();
    await element(by.id('upi-id')).typeText('test@upi');

    await element(by.id('pay-button')).tap();

    // Verify booking success
    await waitFor(element(by.id('booking-success')))
      .toBeVisible()
      .withTimeout(15000);

    await expect(element(by.id('booking-id'))).toBeVisible();
  });

  it('should handle network failure gracefully', async () => {
    // Disable network
    await device.setURLBlacklist(['.*']);

    // Try to search flights
    await element(by.id('search-button')).tap();

    // Should show offline message
    await expect(element(by.text('You are offline'))).toBeVisible();

    // Re-enable network
    await device.clearURLBlacklist();

    // Should auto-retry
    await waitFor(element(by.id('flight-list')))
      .toBeVisible()
      .withTimeout(10000);
  });
});
Enter fullscreen mode Exit fullscreen mode

Production Metrics and Results

const productionMetrics = {
  scale: {
    totalUsers: 127000,
    dailyActiveUsers: 52000,
    monthlyActiveUsers: 98000,
    peakConcurrentUsers: 12500,
    dailyTransactions: 85000,
    totalBookings: 15000000  // Total value
  },

  performance: {
    appStartTime: {
      cold: 1.8,  // seconds
      warm: 0.6
    },
    screenTransition: 92,  // milliseconds
    apiResponseTime: {
      p50: 145,
      p95: 380,
      p99: 520
    },
    crashFreeRate: 99.7,  // percentage
    anr: 0.02  // percentage
  },

  userEngagement: {
    avgSessionLength: '8:30',  // minutes
    dailySessions: 3.2,
    retention: {
      day1: 82,
      day7: 68,
      day30: 54
    },
    appStoreRating: 4.5,
    playStoreRating: 4.4
  },

  technical: {
    bundleSize: {
      android: '28MB',
      ios: '32MB'
    },
    memoryUsage: {
      avg: 95,  // MB
      peak: 142
    },
    batteryImpact: 'Low',
    offlineCapability: '100% core features'
  }
};
Enter fullscreen mode Exit fullscreen mode

Key Learnings

  1. Performance is a Feature: Users expect native-like performance from React Native
  2. Offline-First is Essential: Military bases often have poor connectivity
  3. Native Modules are Powerful: Don't hesitate to write native code when needed
  4. State Management Matters: Redux Toolkit + RTK Query simplified complex state
  5. Monitoring is Crucial: Can't improve what you don't measure

Conclusion

Building a React Native app that scales to 100K+ users taught us that success lies in making the right architectural decisions early, obsessing over performance, and never compromising on user experience. The combination of React Native's cross-platform benefits with strategic native optimizations enabled us to deliver an app that serves critical needs of armed forces personnel reliably, even in the most challenging conditions.

Top comments (0)